update Spring 2021, include support for video player

This commit is contained in:
Godmar Back 2021-04-14 00:29:50 -04:00
parent 9919b5b4b9
commit 07658f3b90
20 changed files with 34564 additions and 6428 deletions

View File

@ -9,16 +9,12 @@ See https://github.com/facebook/create-react-app/blob/master/packages/react-scri
for more details. for more details.
For our server, you have to implement this by making the appropriate For our server, you have to implement this by making the appropriate
changes to how you serve static assets. changes to how you serve static assets if the -a switch is given.
An update to the base code sets up the -a switch to activate this behavior:
https://git.cs.vt.edu/cs3214-staff/pserv/commit/65ee63d43ef92553cd3650b3e276d71e2f7daec5
(Note that the patch doesn't contain the necessary fixes to handle_static_assets
which will be part of future versions of this project.)
The necessary improvement can be made by substituting /index.html as the fname The necessary improvement can be made by substituting /index.html as the fname
when the call to access() fails instead of returning 404. However, for paths when the call to access() fails instead of returning 404. However, for paths
denoting / or existing directories, you must also serve /index.html, which requires denoting / or existing directories, you must also serve /index.html, which requires
a second check to see whether there is an existing directory (and serve /index.html a second check to see whether there is an existing directory (and serve /index.html
if so). if so).
Once you've made this addition, you can test the app as follows: Once you've made this addition, you can test the app as follows:
@ -39,5 +35,7 @@ $ npm run build
$ ./server -p yourport -a -R ../react-app/build $ ./server -p yourport -a -R ../react-app/build
(5) You should now be able to go to the app in http://localhost:10000/ (5) Place one or more .mp4 files into ../react-app/build
(6) You should now be able to go to the app in http://localhost:10000/

40737
react-app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,22 +3,19 @@
"version": "0.2.0", "version": "0.2.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^4.2.4", "bootstrap": "^4.6.0",
"@testing-library/react": "^9.5.0", "formik": "^2.2.6",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"bootstrap": "^4.1.0",
"formik": "2.1.4",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"react-redux": "7.2.0", "react": "^17.0.2",
"react-router-dom": "5.1.2", "react-dom": "^17.0.2",
"react-script": "^2.0.5", "react-player": "^2.9.0",
"react-scripts": "3.4.1", "react-redux": "^7.2.3",
"reactstrap": "8.4.1", "react-router-dom": "^5.2.0",
"redux": "^4.0.0", "react-scripts": "4.0.3",
"redux-thunk": "^2.2.0", "reactstrap": "^8.9.0",
"superagent": "5.2.2", "redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"superagent": "^6.1.0",
"toastr": "^2.1.4" "toastr": "^2.1.4"
}, },
"scripts": { "scripts": {

View File

@ -3,4 +3,5 @@ https://reacttraining.com/react-router/web/guides/redux-integration
Updated Spring 2020 to React 16.13.1 and react-redux 7.2.0 Updated Spring 2020 to React 16.13.1 and react-redux 7.2.0
Updated Spring 2021 to React 17.0.2 and latest version of all other packages.
- added support for video player

22
react-app/src/actions/video.js vendored Normal file
View File

@ -0,0 +1,22 @@
import api from '../api';
import apiAction from './apiAction';
import toastr from 'toastr';
// this function is an "action creator"
// the action created, however, is a thunk.
// see apiAction.js
export function list() {
return apiAction({
baseType: 'VIDEO',
fetch() {
return api.video.list();
},
onSuccess(dispatch, data, getState) {
data.list = data;
toastr.success(`Fetched video list with ${data.list.length} entries`);
},
onError(dispatch, data, getState) {
toastr.success(`Error fetching video list: ${JSON.stringify(data && data.response && data.response.text)}`);
},
});
}

View File

@ -1,5 +1,7 @@
import * as auth from './auth'; import * as auth from './auth';
import * as video from './video';
export default { export default {
auth auth,
video
}; };

5
react-app/src/api/video.js vendored Normal file
View File

@ -0,0 +1,5 @@
import { APIResource, buildURL } from './resource.js';
export function list() {
return new APIResource(buildURL('/video')).get();
}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Row, Label, Input, Alert, Button, ButtonToolbar, Form, FormGroup } from 'reactstrap'; import { Container, Row, Label, Input, Alert, Button, ButtonToolbar, Form, FormGroup } from 'reactstrap';
import { withFormik } from 'formik'; import { withFormik } from 'formik';
/* /*
@ -24,6 +24,7 @@ const LoginForm = (props) => {
return ( return (
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>
<Container>
<Row> <Row>
<FormGroup> <FormGroup>
<Label for="username">User Name</Label> <Label for="username">User Name</Label>
@ -64,6 +65,7 @@ const LoginForm = (props) => {
</Button> </Button>
</ButtonToolbar> </ButtonToolbar>
</Row> </Row>
</Container>
</Form> </Form>
); );
}; };

View File

@ -18,7 +18,8 @@ const menus = {
label: "Private", label: "Private",
onlyifauthenticated: true, onlyifauthenticated: true,
entries: [ entries: [
{ path: `/protected`, label: "Private Content" } { path: `/protected`, label: "Private Content" },
{ path: `/player`, label: "Play MP4" }
] ]
} }
] ]

View File

@ -7,6 +7,7 @@ import PublicPage from '../pages/PublicPage';
import NotFoundPage from '../pages/NotFoundPage'; import NotFoundPage from '../pages/NotFoundPage';
import HomePage from '../pages/HomePage'; import HomePage from '../pages/HomePage';
import PrivatePage from '../pages/PrivatePage'; import PrivatePage from '../pages/PrivatePage';
import PlayerPage from '../pages/PlayerPage';
import { Switch, Route, withRouter } from 'react-router-dom'; import { Switch, Route, withRouter } from 'react-router-dom';
@ -31,6 +32,7 @@ const AppContainer = (props) => (
<Route path={`/login`} component={LoginPage} /> <Route path={`/login`} component={LoginPage} />
<Route path={`/public`} component={PublicPage} /> <Route path={`/public`} component={PublicPage} />
<Route path={`/protected`} component={PrivatePage} /> <Route path={`/protected`} component={PrivatePage} />
<Route path={`/player`} component={PlayerPage} />
<Route component={NotFoundPage} /> <Route component={NotFoundPage} />
</Switch> </Switch>
</div> </div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,27 +1,38 @@
import React from 'react'; import React from 'react';
import { Container, Row, Col } from 'reactstrap'; import { Container, Row, Col } from 'reactstrap';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import logo from './logo.svg'; import logo from '../images/logo.svg';
import cs3214 from '../images/cs3214.png';
let HomePage = (props) => ( let HomePage = (props) => (
<Container> <Container>
<h1>CS3214 Demo App</h1> <Row>
<img alt="" src={logo} className="app-logo" /> <h1>CS3214 Demo App</h1>
</Row>
<Row> <Row>
<Col> <Col>
<p> <img alt="" src={logo} className="app-logo" />
This small <a href="https://reactjs.org/">React {React.version}</a> app </Col>
shows how to use the JWT authentication facilities of your <Col>
server in a progressive single-page web application. <img alt="" src={cs3214} className="cs3214-logo" />
</p>
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col> <Col>
Click <Link to={`/protected`}>here</Link> to <p>
navigate to a protected section of the app. This small <a href="https://reactjs.org/">React {React.version}</a>{" "}
</Col> app shows how to use the JWT authentication facilities of your server
in a progressive single-page web application.
</p>
</Col>
</Row> </Row>
</Container >); <Row>
<Col>
Click <Link to={`/protected`}>here</Link> to navigate to a protected
section of the app.
</Col>
</Row>
</Container>
);
export default HomePage export default HomePage

View File

@ -8,7 +8,7 @@ import LoginForm from '../components/forms/LoginForm';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
const LoginPage = (props) => { const LoginPage = (props) => {
let dispatch = useDispatch(); const dispatch = useDispatch();
function doLogin({username, password}) { function doLogin({username, password}) {
dispatch(login(username, password)); dispatch(login(username, password));
} }

99
react-app/src/pages/PlayerPage.js vendored Normal file
View File

@ -0,0 +1,99 @@
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Container } from 'reactstrap';
import RequireAuthentication from '../containers/RequireAuthentication';
import ReactPlayer from 'react-player/lazy';
import { isLoaded } from '../util/loadingObject'
import { list } from '../actions/video.js';
import { Col, Row, Label, Input, Button, ButtonToolbar, Form, FormGroup, Spinner } from 'reactstrap';
import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
const PlayerPage = () => {
const [dropdownOpen, setDropdownOpen] = useState(false);
const toggle = () => setDropdownOpen(prevState => !prevState);
const dispatch = useDispatch();
let videos = useSelector((state) => state.video);
let [state, setState] = useState({
url: ""
});
const handleChange = (event) => {
setState({
...state,
[event.target.name]: event.target.value
})
}
useEffect(() => {
dispatch(list());
}, []);
const [ playUrl, setPlayUrl ] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
setPlayUrl(state.url);
}
return (
<Container>
<Row>
<h1>Welcome to the Player page</h1>
</Row>
<Row>
<Form onSubmit={handleSubmit}>
<Container>
<Row>
<Col>
<FormGroup>
<Label for="url">Enter URL</Label>
<Input
type="text"
name="url"
value={state.url}
onChange={handleChange}
/>
</FormGroup>
<ButtonToolbar>
<Button type="submit" bsstyle="success" className="mr-2">
Submit
</Button>
</ButtonToolbar>
</Col>
<Col>
or select one from the list of
{!isLoaded(videos) ? (
<Spinner size="lg" color="primary" />
) : (
<Dropdown isOpen={dropdownOpen} toggle={toggle}>
<DropdownToggle caret>Available MP4s</DropdownToggle>
<DropdownMenu>
{videos.list.map((v) => (
<DropdownItem
key={v.name}
onClick={() => setPlayUrl(v.name)}
>
{v.name}
</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
)}
</Col>
</Row>
</Container>
</Form>
</Row>
<Row className="mt-3">
If your implementation of range byte requests works, you should be able
to stream MP4 from your server.
</Row>
<Row className="mt-3">
<ReactPlayer url={playUrl} controls={true} />
</Row>
</Container>
);
};
export default RequireAuthentication(PlayerPage);

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Container } from 'reactstrap'; import { Container, Row } from 'reactstrap';
import RequireAuthentication from '../containers/RequireAuthentication'; import RequireAuthentication from '../containers/RequireAuthentication';
const PrivatePage = () => { const PrivatePage = () => {
@ -8,16 +8,18 @@ const PrivatePage = () => {
return (<Container> return (<Container>
<h1>Welcome to a private page</h1> <h1>Welcome to a private page</h1>
<div> <Row className="mt-3">
You have successfully authenticated as user <tt>{user.sub}</tt>. You have successfully authenticated as user&nbsp;<span>{user.sub}</span>.
</Row>
<Row className="mt-3">
Your token was issued at {new Date(user.iat*1000).toString()}, Your token was issued at {new Date(user.iat*1000).toString()},
it expires {new Date(user.exp*1000).toString()} it expires {new Date(user.exp*1000).toString()}
</div> </Row>
<div> <Row className="mt-3">
This page is "private" only inasmuch as the front-end does not This page is "private" only inasmuch as the front-end does not
display it to unauthenticated users. In a fully-fledged app, display it to unauthenticated users. In a fully-fledged app,
this page would now perform API requests that require authentication. this page would now perform API requests that require authentication.
</div> </Row>
</Container>); </Container>);
} }

View File

@ -19,7 +19,7 @@ const loginHandler = asyncHandler('LOGIN', initialState);
const checkloginHandler = asyncHandler('CHECKLOGIN', initialState); const checkloginHandler = asyncHandler('CHECKLOGIN', initialState);
export default function(state = initialState, action) { export default function(state = initialState, action) {
let newState; let newState = state;
if (action.type.startsWith('LOGIN:')) { if (action.type.startsWith('LOGIN:')) {
newState = loginHandler(state, action); newState = loginHandler(state, action);
} }

View File

@ -1,10 +1,11 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import auth from './auth'; import auth from './auth';
import video from './video';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
auth auth: auth,
video: video
}); });
export default rootReducer; export default rootReducer;

3
react-app/src/reducers/video.js vendored Normal file
View File

@ -0,0 +1,3 @@
import asyncHandler from './asyncHandler';
export default asyncHandler('VIDEO')

View File

@ -11,3 +11,7 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.cs3214-logo {
height: 19vmin;
}