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 our server, you have to implement this by making the appropriate
changes to how you serve static assets.
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.)
changes to how you serve static assets if the -a switch is given.
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
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).
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
(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",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"bootstrap": "^4.1.0",
"formik": "2.1.4",
"bootstrap": "^4.6.0",
"formik": "^2.2.6",
"prop-types": "^15.6.1",
"react-redux": "7.2.0",
"react-router-dom": "5.1.2",
"react-script": "^2.0.5",
"react-scripts": "3.4.1",
"reactstrap": "8.4.1",
"redux": "^4.0.0",
"redux-thunk": "^2.2.0",
"superagent": "5.2.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-player": "^2.9.0",
"react-redux": "^7.2.3",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"reactstrap": "^8.9.0",
"redux": "^4.0.5",
"redux-thunk": "^2.3.0",
"superagent": "^6.1.0",
"toastr": "^2.1.4"
},
"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 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 video from './video';
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 { 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';
/*
@ -24,6 +24,7 @@ const LoginForm = (props) => {
return (
<Form onSubmit={handleSubmit}>
<Container>
<Row>
<FormGroup>
<Label for="username">User Name</Label>
@ -64,6 +65,7 @@ const LoginForm = (props) => {
</Button>
</ButtonToolbar>
</Row>
</Container>
</Form>
);
};

View File

@ -18,7 +18,8 @@ const menus = {
label: "Private",
onlyifauthenticated: true,
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 HomePage from '../pages/HomePage';
import PrivatePage from '../pages/PrivatePage';
import PlayerPage from '../pages/PlayerPage';
import { Switch, Route, withRouter } from 'react-router-dom';
@ -31,6 +32,7 @@ const AppContainer = (props) => (
<Route path={`/login`} component={LoginPage} />
<Route path={`/public`} component={PublicPage} />
<Route path={`/protected`} component={PrivatePage} />
<Route path={`/player`} component={PlayerPage} />
<Route component={NotFoundPage} />
</Switch>
</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 { Container, Row, Col } from 'reactstrap';
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) => (
<Container>
<h1>CS3214 Demo App</h1>
<img alt="" src={logo} className="app-logo" />
<Row>
<h1>CS3214 Demo App</h1>
</Row>
<Row>
<Col>
<p>
This small <a href="https://reactjs.org/">React {React.version}</a> app
shows how to use the JWT authentication facilities of your
server in a progressive single-page web application.
</p>
<img alt="" src={logo} className="app-logo" />
</Col>
<Col>
<img alt="" src={cs3214} className="cs3214-logo" />
</Col>
</Row>
<Row>
<Col>
Click <Link to={`/protected`}>here</Link> to
navigate to a protected section of the app.
</Col>
<p>
This small <a href="https://reactjs.org/">React {React.version}</a>{" "}
app shows how to use the JWT authentication facilities of your server
in a progressive single-page web application.
</p>
</Col>
</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

View File

@ -8,7 +8,7 @@ import LoginForm from '../components/forms/LoginForm';
import { Redirect } from 'react-router-dom';
const LoginPage = (props) => {
let dispatch = useDispatch();
const dispatch = useDispatch();
function doLogin({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 { useSelector } from 'react-redux';
import { Container } from 'reactstrap';
import { Container, Row } from 'reactstrap';
import RequireAuthentication from '../containers/RequireAuthentication';
const PrivatePage = () => {
@ -8,16 +8,18 @@ const PrivatePage = () => {
return (<Container>
<h1>Welcome to a private page</h1>
<div>
You have successfully authenticated as user <tt>{user.sub}</tt>.
<Row className="mt-3">
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()},
it expires {new Date(user.exp*1000).toString()}
</div>
<div>
</Row>
<Row className="mt-3">
This page is "private" only inasmuch as the front-end does not
display it to unauthenticated users. In a fully-fledged app,
this page would now perform API requests that require authentication.
</div>
</Row>
</Container>);
}

View File

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

View File

@ -1,10 +1,11 @@
import { combineReducers } from 'redux';
import auth from './auth';
import video from './video';
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);
}
}
.cs3214-logo {
height: 19vmin;
}