added React.js example; see README.first

This commit is contained in:
Godmar Back 2018-04-29 18:11:45 -04:00
parent 65ee63d43e
commit 20aeca4b19
34 changed files with 15463 additions and 0 deletions

21
react-app/.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

43
react-app/README.first Normal file
View File

@ -0,0 +1,43 @@
This directory contains an example of an React application that can interact
with your server.
To use it, you have to add a small feature to your server called HTML5 fallback,
which is the ability to serve /index.html if it cannot find a file at the path
specified.
See https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#deployment
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.)
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
if so).
Once you've made this addition, you can test the app as follows:
(1) ssh into rlogin with
$ ssh -L 10000:localhost:yourport you@rlogin.cs.vt.edu
(2) Add node to your PATH:
$ export PATH=/home/courses/cs3214/software/node-v8.11.1-linux-x64/bin:$PATH
(3) Inside react-app, run
$ npm run build
(4) and then run your server with
$ ./server -p yourport -a -R ../react-app/build
(5) You should now be able to go to the app in http://localhost:10000/

2444
react-app/README.md Normal file

File diff suppressed because it is too large Load Diff

11914
react-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
react-app/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "react-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "^4.1.0",
"formik": "^0.11.11",
"prop-types": "^15.6.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"react-scripts": "1.1.4",
"reactstrap": "^5.0.0",
"redux": "^4.0.0",
"redux-thunk": "^2.2.0",
"superagent": "^3.8.2",
"toastr": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:9999/"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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`.
-->
<!--
<script src="%PUBLIC_URL%/api/login" type="text/javascript"></script>
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
react-app/src/NOTES Normal file
View File

@ -0,0 +1,3 @@
https://reacttraining.com/react-router/web/guides/redux-integration

53
react-app/src/actions/apiAction.js vendored Normal file
View File

@ -0,0 +1,53 @@
/**
* This is an action creator for actions that relate to accessing APIs.
* Whereas normal action creators return a plain { type: ..., ... } action
* object, this action creator returns a function. This is possible
* through the use of the redux-thunk middleware, see store.js
* See https://github.com/gaearon/redux-thunk
*
* Courtesy: Scott Pruett
*/
export default function apiAction(
{
baseType, // the base type of the action, e.g. "LOGIN", etc.
fetch, // a promise-returning function
onSuccess, // a callback for when promise succeeded
onError, // a callback for when promise failed
params // params - passed backed to callbacks
}
) {
return (dispatch, getState) => {
// dispatch *:BEGIN action to let everyone know that API request
// has begun. (Reducers can now set 'loadingStatus' to loading.)
dispatch({
type: baseType + ':BEGIN',
params,
});
// initiate the API request
fetch().then(
response => {
if (onSuccess) {
onSuccess(dispatch, response.body, getState);
}
// on success, dispatch *:OK action with response
dispatch({
type: baseType + ':OK',
response: response.body,
params,
});
},
error => {
if (onError) {
onError(dispatch, error, getState);
}
// on error, dispatch *:ERROR action with response
dispatch({
type: baseType + ':ERROR',
error: error,
params,
});
}
);
};
}

30
react-app/src/actions/auth.js vendored Normal file
View File

@ -0,0 +1,30 @@
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 login(username, password) {
return apiAction({
baseType: 'LOGIN',
fetch() {
return api.auth.login({ username, password });
},
onSuccess(dispatch, data, getState) {
toastr.success(`Success logging in: ${JSON.stringify(data)}`);
},
});
}
export function checklogin() {
return apiAction({
baseType: 'CHECKLOGIN',
fetch() {
return api.auth.checklogin();
},
onSuccess(dispatch, data, getState) {
toastr.success(`Server says ${JSON.stringify(data)}`);
},
});
}

16
react-app/src/api/auth.js vendored Normal file
View File

@ -0,0 +1,16 @@
import { APIResource, buildURL } from './resource.js';
export function login({username, password}) {
return new APIResource(buildURL('/login')).post(
{ username, password }
);
}
export function checklogin() {
return new APIResource(buildURL('/login')).get();
}
export function logout() {
return new APIResource(buildURL('/logout')).post();
}

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

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

42
react-app/src/api/resource.js vendored Normal file
View File

@ -0,0 +1,42 @@
// courtesy: Scott Pruett
/*
* The purpose of this file is to separate the specific 'fetch' library -
* here, superagent, from the rest of the application.
*/
import { sprintf } from 'sprintf-js';
import request from 'superagent';
// Side note:
// import { apiprefix } from '../config/';
// does not work because 'apiprefix' is not a named export of config!
import config from '../config/';
export function buildURL(/* template, printf arguments... */) {
const url = sprintf.apply(null, arguments);
return {
builtURL: config.apiPrefix + url,
};
}
export class APIResource {
constructor(url) {
//invariant(url != null, 'No URL given for APIResource');
//invariant(url.builtURL != null, 'URLs must be made with buildURL');
this.url = url.builtURL;
}
get(params) {
// as per https://visionmedia.github.io/superagent/#setting-accept
return request.get(this.url).accept('json').query(params);
}
put(data) {
return request.put(this.url).send(data);
}
post(data) {
return request.post(this.url).send(data);
}
delete() {
return request.delete(this.url);
}
}

17
react-app/src/components/Logout.js vendored Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
import { Redirect } from 'react-router-dom';
import store from '../store';
class Logout extends React.Component {
componentWillMount() {
// normally, we would inform the server just in case.
document.cookie = "auth_token=";
store.dispatch({ type: "LOGOUT" });
}
render() {
return (<Redirect to="/" />);
}
}
export default Logout;

123
react-app/src/components/TopNavBar.js vendored Normal file
View File

@ -0,0 +1,123 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem,
NavLink,
UncontrolledDropdown,
DropdownToggle,
DropdownMenu,
DropdownItem } from 'reactstrap';
// https://stackoverflow.com/questions/42372179/reactstrap-and-react-router-4-0-0-beta-6-active-navlink
import { NavLink as RRNavLink } from 'react-router-dom';
import { isLoaded } from '../util/loadingObject'
/* A helper component to render an array of dropdowns
* inside a <Nav> element
*/
const DropDowns = (props) => {
const user = props.user
const isAdmin = Boolean(Number(user.admin))
return (<div>
{props.dropdowns.map((dropdown, index) =>
(!(dropdown.onlyifauthenticated || dropdown.onlyifadmin)
|| (isLoaded(user) &&
(
(dropdown.onlyifauthenticated && !dropdown.onlyifadmin)
|| (dropdown.onlyifadmin && isAdmin)
)
)
) &&
<UncontrolledDropdown nav inNavbar key={index}>
<DropdownToggle nav caret>
{dropdown.label}
</DropdownToggle>
<DropdownMenu right>
{dropdown.entries.map((item) =>
<DropdownItem key={item.path}>
<NavLink to={item.path} key={item.path} activeClassName="active" tag={RRNavLink}>
{item.label}
</NavLink>
</DropdownItem>
)}
</DropdownMenu>
</UncontrolledDropdown>
)}</div>)
}
/**
* Navigation bar component
*/
class NavBar extends React.Component {
static propTypes = {
menus: PropTypes.object,
user: PropTypes.object,
branding: PropTypes.string,
}
constructor(props) {
super(props);
this.toggle = this.toggle.bind(this);
this.state = {
isOpen: false
};
}
toggle() {
this.setState({
isOpen: !this.state.isOpen
});
}
render() {
const menus = this.props.menus
const user = this.props.user
return (
<div>
<Navbar color="light" light expand="md">
<NavbarToggler onClick={this.toggle} />
<NavbarBrand to="/">{this.props.branding}</NavbarBrand>
<Collapse isOpen={this.state.isOpen} navbar>
<Nav className="mr-auto" navbar>
{menus.topbar.map((item) =>
<NavItem key={item.path}>
<NavLink to={item.path} activeClassName="active" tag={RRNavLink}>
{item.label}
</NavLink>
</NavItem>
)}
{ menus.leftdropdowns &&
<DropDowns className="mr-auto" dropdowns={menus.leftdropdowns} user={user} />
}
</Nav>
<Nav className="ml-auto">
{ menus.rightdropdowns &&
<DropDowns className="ml-auto" dropdowns={menus.rightdropdowns} user={user} />
}
{isLoaded(user) ?
<NavItem>
<NavLink activeClassName="active" tag={RRNavLink} to={this.props.logoutUrl}>Logout ({user.sub})</NavLink>
</NavItem>
:
<NavItem>
<NavLink activeClassName="active" tag={RRNavLink} to={this.props.loginUrl}>Login</NavLink>
</NavItem>
}
</Nav>
</Collapse>
</Navbar>
</div>
);
}
}
export default NavBar;

View File

@ -0,0 +1,89 @@
import React from 'react';
import { Row, Label, Input, Alert, Button, ButtonToolbar, Form, FormGroup } from 'reactstrap';
import { withFormik } from 'formik';
/*
* This is a connected form that display username/password.
* When the user hits submit, the 'onSubmit' method is called
* in the parent, which receives the username/password the user
* entered. It also performs validation.
*/
class LoginForm extends React.Component {
render() {
const {
handleSubmit, // rest is from HOC
isSubmitting,
handleReset,
handleBlur,
handleChange,
// errors,
dirty,
// touched,
values,
// valid
} = this.props;
return (
<Form onSubmit={handleSubmit}>
<Row>
<FormGroup>
<Label for="username">User Name</Label>
<Input type="text" name="username" value={values.username}
onChange={handleChange}
onBlur={handleBlur}
/>
</FormGroup>
<FormGroup>
<Label for="password">Password</Label>
<Input type="password" name="password"
onChange={handleChange}
onBlur={handleBlur}
/>
</FormGroup>
{ this.props.autherror &&
<FormGroup>
<Alert bsstyle='danger'>
{this.props.autherror.message || 'Login failed'}
</Alert>
</FormGroup>
}
</Row>
<Row>
<ButtonToolbar>
<Button
type='submit'
bsstyle='success' className="mr-2">
Submit
</Button>
<Button
type="button"
onClick={handleReset}
disabled={!dirty || isSubmitting}>
Reset
</Button>
</ButtonToolbar>
</Row>
</Form>
);
}
}
/*
function mapStateToProps(state) {
return {
autherror: state.auth.error
};
}
*/
export default withFormik({
mapPropsToValues: () => ({ username: '', password: '' }),
handleSubmit: (values, { setSubmitting, props }) => {
props.onSubmit(values);
setSubmitting(false);
},
displayName: 'LoginForm', // helps with React DevTools
})(LoginForm);

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { isLoaded } from '../util/loadingObject';
// From https://github.com/gillisd/react-router-v4-redux-auth
const PrivateRoute = ({component: ComposedComponent, ...rest}) => {
class Authentication extends React.Component {
// redirect if not authenticated; otherwise,
// return the component input into <PrivateRoute />
handleRender(props) {
if (!this.props.authenticated) {
return <Redirect to={{
pathname: '/login',
state: {
from: props.location
}
}}/>
} else {
return <ComposedComponent {...props}/>
}
}
render() {
return (
<Route {...rest} render={this.handleRender.bind(this)}/>
)
}
}
function mapStateToProps(state) {
return {authenticated: isLoaded(state.auth)};
}
const AuthenticationContainer = connect(mapStateToProps)(Authentication)
return <AuthenticationContainer/>
}
export { PrivateRoute };

36
react-app/src/config/index.js vendored Normal file
View File

@ -0,0 +1,36 @@
// public URL, put in by create-react-app's webpack config
const publicUrl = process.env.PUBLIC_URL
const menus = {
topbar : [
{ path: `${publicUrl}/`, label: "Home" },
],
leftdropdowns : [
{
label: "Public",
entries: [
{ path: `${publicUrl}/public`, label: "Public Content" }
]
}
],
rightdropdowns : [
{
label: "Private",
onlyifauthenticated: true,
entries: [
{ path: `${publicUrl}/protected`, label: "Private Content" }
]
}
]
};
// To avoid same-origin issues, make API requests to origin.
// webpack devserver will proxy these requests, but you must specify
// a 'proxy' entry in package.json, see
// https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md#proxying-api-requests-in-development
const apiPrefix = `${publicUrl}/api`
console.log(`Read configuration. Public_URL: ${publicUrl}`)
// console.log(`apiPrefix: ${apiPrefix}`)
export default { menus, apiPrefix, publicUrl }

View File

@ -0,0 +1,57 @@
import React from 'react';
import { connect } from 'react-redux';
import TopNavBar from '../components/TopNavBar';
import Logout from '../components/Logout';
import LoginPage from '../pages/LoginPage';
import PublicPage from '../pages/PublicPage';
import NotFoundPage from '../pages/NotFoundPage';
import HomePage from '../pages/HomePage';
import PrivatePage from '../pages/PrivatePage';
import { PrivateRoute } from '../components/privateroute';
import { Switch, Route, withRouter } from 'react-router-dom';
import config from '../config/';
/** AppContainer renders the navigation bar on top and its
* children in the main part of the page. Its children will
* be chosen based on the selected route.
*/
class AppContainer extends React.Component {
render() {
return (
<div>
<TopNavBar branding="CS3214 Demo App"
menus={config.menus}
user={this.props.user}
loginUrl={`${config.publicUrl}/login`}
logoutUrl={`${config.publicUrl}/logout`}
/>
<div className="container-fluid marketing">
<Switch>
<Route exact path={`${config.publicUrl}/`} component={HomePage} />
<Route path={`${config.publicUrl}/logout`} component={Logout} />
<Route path={`${config.publicUrl}/login`} component={LoginPage} />
<Route path={`${config.publicUrl}/public`} component={PublicPage} />
<PrivateRoute path={`${config.publicUrl}/protected`} component={PrivatePage} />
<Route component={NotFoundPage} />
</Switch>
</div>
</div>
);
}
}
function mapStateToProps(state) {
return {
user: state.auth
};
}
function mapDispatchToProps(dispatch) {
return {
dispatch
};
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AppContainer));

33
react-app/src/index.js vendored Normal file
View File

@ -0,0 +1,33 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import 'bootstrap/dist/css/bootstrap.css'
import './style/logo.css';
import toastr from 'toastr';
import 'toastr/build/toastr.min.css';
import jQuery from 'jquery'; // for toastr;
import AppContainer from './containers/AppContainer';
import store from './store';
import {checklogin} from './actions/auth';
toastr.options.closeButton = true;
toastr.options.positionClass = 'toast-bottom-right';
// request /api/login with current cookie (if any) to see if we're logged in.
store.dispatch(checklogin());
const mountPoint = document.getElementById('root');
const rootNode = (
<Provider store={store}>
<Router>
<AppContainer />
</Router>
</Provider>
);
ReactDOM.render(rootNode, mountPoint);
window.jQuery = jQuery; // for toastr

32
react-app/src/pages/HomePage.js vendored Normal file
View File

@ -0,0 +1,32 @@
import React from 'react';
import { Container, Row, Col } from 'reactstrap';
import { Link } from 'react-router-dom';
import config from '../config/';
import logo from './logo.svg';
class HomePage extends React.Component {
render() {
return (
<Container>
<h1>CS3214 Demo App</h1>
<img alt="" src={logo} className="app-logo" />
<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>
</Col>
</Row>
<Row>
<Col>
Click <Link to={`${config.publicUrl}/protected`}>here</Link> to
navigate to a protected section of the app.
</Col>
</Row>
</Container>);
}
}
export default HomePage

59
react-app/src/pages/LoginPage.js vendored Normal file
View File

@ -0,0 +1,59 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Card, CardHeader, CardBody, Container, Row, Col } from 'reactstrap';
import { login } from '../actions/auth.js';
import { isLoading, isLoaded } from '../util/loadingObject'
import LoginForm from '../components/forms/LoginForm';
import { Redirect } from 'react-router-dom';
class LoginPage extends React.Component {
static contextTypes = {
router: PropTypes.object.isRequired
}
static propTypes = {
user: PropTypes.object.isRequired
}
doLogin({username, password}) {
this.props.dispatch(login(username, password));
}
render() {
const user = this.props.user;
const isAuthenticated = isLoaded(user);
const { from } = this.props.location.state || { from: { pathname: "/" } };
if (isAuthenticated) {
return (<Redirect to={from} />);
}
return (
<Container>
<Row className="pb-5 pt-5">
<Col xsoffset={0} xs={10} smoffset={4} sm={4}>
<Card>
<CardHeader><h3>Please log in</h3></CardHeader>
<CardBody>
<LoginForm
loading={isLoading(user)}
autherror={user.error}
onSubmit={v => this.doLogin(v)} />
</CardBody>
</Card>
</Col>
</Row>
</Container>
);
}
}
function mapStateToProps(state) {
return {
user: state.auth
};
}
export default connect(mapStateToProps)(LoginPage);

15
react-app/src/pages/NotFoundPage.js vendored Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
/* Example of 'stateless function' syntax to define React components.
* See https://facebook.github.io/react/docs/reusable-components.html#stateless-functions
*/
const NotFound = (props) =>
<div><h1>Route not found Error</h1>
<div>
Something went wrong: {window.location.pathname} could not be found!
</div>
{/* JSON.stringify(props) */}
</div>
export default NotFound

21
react-app/src/pages/PrivatePage.js vendored Normal file
View File

@ -0,0 +1,21 @@
import React from 'react';
import { Container } from 'reactstrap';
class PrivatePage extends React.Component {
render() {
return (
<Container>
<h1>Welcome to a private page</h1>
<div>
You have successfully authenticated.
</div>
<div>
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>
</Container>);
}
}
export default PrivatePage

16
react-app/src/pages/PublicPage.js vendored Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import { Container } from 'reactstrap';
class PublicPage extends React.Component {
render() {
return (
<Container>
<h1>Welcome to a public page</h1>
<div>
This public page is accessible to anyone.
</div>
</Container>);
}
}
export default PublicPage

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

34
react-app/src/reducers/asyncHandler.js vendored Normal file
View File

@ -0,0 +1,34 @@
// Courtesy: Scott Pruett
//
// Returns a reducer that can process the following actions on baseType:
// :BEGIN - object is now loading
// :OK - object has loaded, apply transformResponse if any
// :ERROR - an error was encountering during loading
//
// The reduced state includes a 'loadingStatus' property: { loading, error, ok }
//
import { errorObject, loadedObject, loadingObject } from '../util/loadingObject';
export default function asyncHandler(baseType, initialState, transformResponse) {
return (state, action) => {
if (state == null || action.type === baseType + ':RESET') {
if (initialState == null) {
return loadingObject();
}
return initialState;
}
if (action.type === baseType + ':BEGIN') {
return loadingObject(state);
} else if (action.type === baseType + ':OK') {
let response = action.response;
if (transformResponse != null) {
response = transformResponse(response);
}
return loadedObject(response);
} else if (action.type === baseType + ':ERROR') {
return errorObject({ ...state, error: action.error });
}
return state;
};
}

37
react-app/src/reducers/auth.js vendored Normal file
View File

@ -0,0 +1,37 @@
/* Reducer.
*
* Internal state is as follows:
* {
* auth: {
* sub:
* iat: ...
* exp: ...
* loadingStatus: ok/loading/error
* }
* }
*/
import asyncHandler from './asyncHandler';
// we could get the initial state also via an <script ....> in public/index.html
let initialState = { };
const loginHandler = asyncHandler('LOGIN', initialState);
const checkloginHandler = asyncHandler('CHECKLOGIN', initialState);
export default function(state = initialState, action) {
let newState;
if (action.type === 'LOGIN:OK') {
newState = loginHandler(state, action);
}
if (action.type === 'CHECKLOGIN:OK') {
// a bit hacky.
newState = checkloginHandler(state, action);
if (!('sub' in newState))
delete newState.loadingStatus;
}
if (action.type === 'LOGOUT') {
newState = { }
}
return { ...newState };
}

10
react-app/src/reducers/root.js vendored Normal file
View File

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

117
react-app/src/registerServiceWorker.js vendored Normal file
View File

@ -0,0 +1,117 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

14
react-app/src/store.js vendored Normal file
View File

@ -0,0 +1,14 @@
import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/root';
const createStoreWithMiddleware = compose(
applyMiddleware(thunk),
window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore);
const store = createStoreWithMiddleware(rootReducer);
export default store;

View File

@ -0,0 +1,13 @@
.app-logo {
animation: app-logo-spin infinite 20s linear;
height: 20vmin;
}
@keyframes app-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

35
react-app/src/util/loadingObject.js vendored Normal file
View File

@ -0,0 +1,35 @@
// Courtesy: Scott Pruett
// manage the loading of objects from an external source
// specifically, an object can be in state 'loading', followed
// by either 'ok' or 'error'
export function loadingObject(object = {}) {
return { ...object,
loadingStatus: 'loading',
error: undefined,
};
}
export function loadedObject(object) {
return { ...object,
loadingStatus: 'ok',
error: undefined,
};
}
export function errorObject(object) {
return { ...object,
loadingStatus: 'error',
};
}
export function isLoading(object) {
return object.loadingStatus === 'loading';
}
export function isError(object) {
return object.loadingStatus === 'error';
}
export function isLoaded(object) {
return object.loadingStatus === 'ok';
}