Merge branch 'feature/video-player' of 858/frontend into develop

This commit is contained in:
sanj 2018-12-07 22:43:41 +00:00 committed by Gitea
commit 7d8778b51c
12 changed files with 128 additions and 25 deletions

View File

@ -19,7 +19,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>858</title>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

@ -1,4 +1,11 @@
// Topics Action Types
export const START_LOADING_ALL_TOPICS = 'START_LOADING_ALL_TOPICS'; export const START_LOADING_ALL_TOPICS = 'START_LOADING_ALL_TOPICS';
export const LOADED_ALL_TOPICS = 'LOADED_ALL_TOPICS'; export const LOADED_ALL_TOPICS = 'LOADED_ALL_TOPICS';
export const START_LOADING_RANDOM_TOPIC = 'START_LOADING_RANDOM_TOPIC'; export const START_LOADING_RANDOM_TOPIC = 'START_LOADING_RANDOM_TOPIC';
export const LOADED_RANDOM_TOPIC = 'LOADED_RANDOM_TOPIC'; export const LOADED_RANDOM_TOPIC = 'LOADED_RANDOM_TOPIC';
// Videos Action Types
export const ADD_VIDEOS_TO_STATE = 'ADD_VIDEOS_TO_STATE';
export const START_LOADING_VIDEO = 'START_LOADING_VIDEO';
export const LOADED_VIDEO = 'LOADED_VIDEO';

View File

@ -2,6 +2,7 @@ import { fetchAllTopics, fetchVideosByTopic } from '../utils/api';
import { APIError } from './errors'; import { APIError } from './errors';
import { getItem, setItem } from '../utils/safe-storage'; import { getItem, setItem } from '../utils/safe-storage';
import getRandomTopic from '../utils/get-random-topic'; import getRandomTopic from '../utils/get-random-topic';
import { addVideosToState } from './videos';
import { import {
START_LOADING_ALL_TOPICS, START_LOADING_ALL_TOPICS,
LOADED_ALL_TOPICS, LOADED_ALL_TOPICS,
@ -42,6 +43,7 @@ export function getRandomTopicVideos(allTopics, numVideos=4) {
dispatch(loadingRandomTopicVideos()); dispatch(loadingRandomTopicVideos());
fetchVideosByTopic(randomTopic.name, 0, 4) fetchVideosByTopic(randomTopic.name, 0, 4)
.then(videos => { .then(videos => {
dispatch(addVideosToState(videos));
dispatch(loadedRandomTopicVideos(randomTopic, videos)); dispatch(loadedRandomTopicVideos(randomTopic, videos));
}); });
}; };

8
src/actions/videos.js Normal file
View File

@ -0,0 +1,8 @@
import { ADD_VIDEOS_TO_STATE } from './action_types';
export function addVideosToState(videos) {
return {
type: ADD_VIDEOS_TO_STATE,
payload: videos
};
}

View File

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import VideoItem from "./VideoItem"; import VideoItem from "./VideoItem";
import SectionHeading from "./SectionHeading"; import SectionHeading from "./SectionHeading";
import {Link} from 'react-router-dom'; import { Link } from 'react-router-dom';
class RandomTopic extends React.Component { class RandomTopic extends React.Component {
@ -15,7 +15,7 @@ class RandomTopic extends React.Component {
width={450} width={450}
gap={16}> gap={16}>
{this.props.videos.map(video => {this.props.videos.map(video =>
<VideoItem id={video.id} title={video.title}/> <VideoItem id={video.id} key={video.id} title={video.title}/>
)} )}
<Link to="/topics"> <Link to="/topics">
See All Topics See All Topics

View File

@ -1,16 +1,20 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { getThumbnail } from '../utils/video'; import { getThumbnail } from '../utils/video';
class VideoItem extends React.Component { class VideoItem extends React.Component {
render() { render() {
const videoLink = `/videos/${this.props.id}`;
return ( return (
<Link to={ videoLink }>
<section className="video-item"> <section className="video-item">
<section className="video-thumbnail-container"> <section className="video-thumbnail-container">
<img className="video-thumbnail" src={ getThumbnail(this.props.id) } alt=""/> <img className="video-thumbnail" src={ getThumbnail(this.props.id) } alt=""/>
</section> </section>
<h3 className="video-title">{ this.props.title }</h3> <h3 className="video-title">{ this.props.title }</h3>
</section> </section>
</Link>
) )
} }
} }

View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import Grid from 'react-css-grid'; import Grid from 'react-css-grid';
import PropTypes from 'prop-types';
import { getVideo } from '../utils/video';
class VideoPlayer extends React.Component { class VideoPlayer extends React.Component {
render() { render() {
@ -8,19 +10,13 @@ class VideoPlayer extends React.Component {
<Grid width={450} <Grid width={450}
gap={16}> gap={16}>
<section> <section>
<h2>Title</h2> <h2>{ this.props.title }</h2>
<video src="https://video22.858.ma/BUR/240p3.mp4" controls> <video src={ getVideo(this.props.id) } controls>
</video> </video>
<h3>date mm/dd/yy </h3> <h3>date { this.props.date } </h3>
</section> </section>
<section className="text-left"> <section className="text-left">
<p>Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the <p>{ this.props.description }</p>
industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and
scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into
electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of
Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like
Aldus
PageMaker including versions of Lorem Ipsum.</p>
</section> </section>
</Grid> </Grid>
@ -28,4 +24,10 @@ class VideoPlayer extends React.Component {
} }
} }
export default (VideoPlayer); VideoPlayer.propTypes = {
title: PropTypes.string,
date: PropTypes.string,
description: PropTypes.string
};
export default VideoPlayer;

View File

@ -1,14 +1,56 @@
import React from 'react'; import React from 'react';
import {connect} from 'react-redux';
import VideoPlayer from "../components/VideoPlayer"; import VideoPlayer from "../components/VideoPlayer";
import { fetchVideoById } from '../utils/api';
class Video extends React.Component { class Video extends React.Component {
constructor() {
super();
this.state = {
currentVideo: null
};
}
componentDidMount() {
// get videoId from the URL
const videoId = this.props.match.params.videoId;
// if data for videoId is already part of allVideos, setState to currentVideo
if (this.props.allVideos && this.props.allVideos.hasOwnProperty(videoId)) {
const currentVideo = this.props.allVideos[videoId];
this.setState({currentVideo: currentVideo});
} else {
// video is not part of allVideos, fetch it from the API
fetchVideoById(videoId)
.then(video => {
this.setState({
currentVideo: video
});
});
}
}
render() { render() {
const video = this.state.currentVideo;
return ( return (
<section> <section>
<VideoPlayer/> { video &&
<VideoPlayer
id={ video.id }
title={ video.title }
date = { video.date }
description={ video.description }
/>
}
</section> </section>
); );
} }
} }
export default (Video);
const mapStateToProps = state => ({
allVideos: state.videos.allVideos
});
export default connect(mapStateToProps)(Video);

View File

@ -1,9 +1,11 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import TopicsReducer from './topics' import TopicsReducer from './topics';
import VideosReducer from './videos';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
topics: TopicsReducer topics: TopicsReducer,
videos: VideosReducer
}); });
export default rootReducer; export default rootReducer;

22
src/reducers/videos.js Normal file
View File

@ -0,0 +1,22 @@
import {
ADD_VIDEOS_TO_STATE
} from '../actions/action_types';
export default function(state={}, action) {
switch (action.type) {
case ADD_VIDEOS_TO_STATE:
let clonedState = Object.assign({}, state);
if (!clonedState.allVideos) {
clonedState.allVideos = {};
}
const videosToAdd = action.payload;
videosToAdd.forEach(video => {
if (!clonedState.allVideos.hasOwnProperty(video.id)) {
clonedState.allVideos[video.id] = video;
}
});
return clonedState;
default:
return state;
}
}

View File

@ -1,6 +1,16 @@
import config from '../config'; import config from '../config';
import 'whatwg-fetch'; import 'whatwg-fetch';
const VIDEO_KEYS = [
"title",
"source",
"project",
"topic",
"language",
"duration",
"id",
"date"
];
/* /*
Base function to make API calls Base function to make API calls
@ -98,7 +108,7 @@ export function fetchAllDates(startRange=0, endRange=1000) {
*/ */
export function fetchVideosByX(key, value, startRange, endRange, sortKey='random') { export function fetchVideosByX(key, value, startRange, endRange, sortKey='random') {
const data = { const data = {
"keys": ["editable", "modified", "title", "source", "project", "topic", "language", "duration", "id"], "keys": VIDEO_KEYS,
"query": { "query": {
"conditions": [{ "conditions": [{
"key": key, "key": key,
@ -205,3 +215,7 @@ export function fetchRandomPlace() {
export function fetchVideosByPlace(placeId, startRange=0, endRange=100, sortKey='random') { export function fetchVideosByPlace(placeId, startRange=0, endRange=100, sortKey='random') {
return fetchVideosByX('place', placeId, startRange, endRange, sortKey); return fetchVideosByX('place', placeId, startRange, endRange, sortKey);
} }
export function fetchVideoById(id) {
return callApi('get', {'id': id, keys: VIDEO_KEYS});
}

View File

@ -5,5 +5,5 @@ export function getThumbnail(id, size='256') {
} }
export function getVideo(id, size='480') { export function getVideo(id, size='480') {
return `${config.videoBase}/${id}/${size}.mp4`; return `${config.videoBase}/${id}/${size}p.mp4`;
} }