Add modal and plex token form

This commit is contained in:
mjrode
2019-04-05 21:38:47 -05:00
parent 254c0784d9
commit 2e7421f342
18 changed files with 527 additions and 116 deletions

View File

@@ -2657,7 +2657,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@@ -2675,11 +2676,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -2692,15 +2695,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -2803,7 +2809,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -2813,6 +2820,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -2825,17 +2833,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -2852,6 +2863,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -2924,7 +2936,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -2934,6 +2947,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -3009,7 +3023,8 @@
},
"safe-buffer": {
"version": "5.1.2",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -3039,6 +3054,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -3056,6 +3072,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -3094,11 +3111,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true
"bundled": true,
"optional": true
}
}
},
@@ -5720,7 +5739,8 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@@ -5738,11 +5758,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5755,15 +5777,18 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true
"bundled": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -5866,7 +5891,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -5876,6 +5902,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -5888,17 +5915,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true
"bundled": true,
"optional": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -5915,6 +5945,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -5987,7 +6018,8 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -5997,6 +6029,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -6072,7 +6105,8 @@
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true
"bundled": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -6102,6 +6136,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -6119,6 +6154,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -6157,11 +6193,13 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true
"bundled": true,
"optional": true
},
"yallist": {
"version": "3.0.2",
"bundled": true
"bundled": true,
"optional": true
}
}
},

BIN
client/public/favicon.ico Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -32,7 +32,7 @@
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`.
-->
<title>React App</title>
<title>PlexRex</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,8 +1,9 @@
import axios from 'axios';
import {FETCH_USER, FETCH_PLEX_TOKEN} from './types';
// Action Creators
export const fetchUser = () => async dispatch => {
const res = await axios.get('/auth/current_user');
const res = await axios.get('/api/auth/current_user');
dispatch({type: FETCH_USER, payload: res.data});
};

View File

@@ -1,27 +1,19 @@
import React, {Component} from 'react';
import {BrowserRouter, Route} from 'react-router-dom';
import {connect} from 'react-redux';
import axios from 'axios';
import * as actions from '../actions';
import Header from './Header';
import Landing from './Landing';
import Dashboard from './Dashboard';
import Hero from './Hero';
import CreateAccount from './CreateAccount';
import PlexForm from './plex/PlexForm';
// const Dashboard = () => <h2>Dashboard</h2>;
// const SurveyNew = () => <h2>SurveyNew</h2>;
class App extends Component {
state = {user: {}};
componentDidMount() {
this.fetchUser();
this.props.fetchUser();
}
fetchUser = async () => {
const res = await axios.get('/auth/current_user');
this.setState({user: res.data});
console.log(res.data);
return res;
};
render() {
return (
<div>
@@ -29,13 +21,10 @@ class App extends Component {
<BrowserRouter>
<div className="container">
<Header />
<Route exact path="/" component={Landing} />
<Route exact path="/surveys" component={Dashboard} />
<Route exact path="/" component={Hero} />
<Route
path="/app/plex"
render={props => <PlexForm user={this.state.user} />}
/>
<Route path="/create-account" component={CreateAccount} />
<Route path="/form" component={PlexForm} />
</div>
</BrowserRouter>
</div>
@@ -44,4 +33,7 @@ class App extends Component {
}
}
export default App;
export default connect(
null,
actions,
)(App);

View File

@@ -0,0 +1,7 @@
.no-bottom-margin {
margin-bottom: 0px;
}
.margin-spacing {
margin: 3em 0;
}

View File

@@ -0,0 +1,116 @@
import React from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import CssBaseline from '@material-ui/core/CssBaseline';
import {connect} from 'react-redux';
import {withStyles} from '@material-ui/core/styles';
import './CreateAccount.css';
import Modal from './Modal';
import styles from './css';
class CreateAccount extends React.Component {
state = {email: '', password: '', section_data: ''};
onFormSubmit = event => {
event.preventDefault();
this.getPlexToken(this.state);
};
getPlexToken = async params => {
const res = await axios.get('/plex/auth', {params});
return res;
};
fetchSections = async params => {
const res = await axios.get('/plex/library/sections');
console.log(res);
this.setState({section_data: res.data});
};
render() {
const {classes} = this.props;
return (
<React.Fragment>
<CssBaseline />
<div className={classes.heroUnit}>
<div className={classes.heroContentSmall}>
<div className="section center-align">
<h3 className={classes.shrinkTopMargin}>Fetch Plex Token</h3>
<div className="center">
<Modal />
</div>
</div>
<div className="section center-align">
<div className="row">
<form onSubmit={this.onFormSubmit} className="col s12">
<div className="row no-bottom-margin">
<div className="input-field col m8 offset-m2 s12">
<p>Plex Email</p>
<input
id="email"
type="text"
className="validate center-align"
value={this.state.email}
onChange={e => this.setState({email: e.target.value})}
/>
</div>
</div>
<div className="row no-bottom-margin">
<div className="input-field col m8 offset-m2 s12">
<p>Plex Server URL</p>
<input
id="email"
type="text"
className="validate center-align"
value={this.state.email}
onChange={e => this.setState({email: e.target.value})}
/>
</div>
</div>
<div className="row no-bottom-margin">
<div className="input-field col m8 offset-m2 s12">
<p>Plex Password</p>
<input
id="password"
type="password"
className="validate center-align"
value={this.state.password}
onChange={e =>
this.setState({password: e.target.value})
}
/>
</div>
</div>
<div className="row">
<div className="col s12">
<div className="center-align">
<button
className="btn-large waves-effect waves-light center-align"
type="submit"
name="action"
>
Submit
<i className="material-icons right">send</i>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</React.Fragment>
);
}
}
CreateAccount.propTypes = {
classes: PropTypes.object.isRequired,
};
function mapStateToProps({auth}) {
return {auth};
}
export default connect(mapStateToProps)(withStyles(styles)(CreateAccount));

View File

@@ -1,8 +0,0 @@
import React from 'react';
// import {Link} from 'react-router-dom';
const Dashboard = () => {
return <div>Dashboard</div>;
};
export default Dashboard;

View File

@@ -4,28 +4,20 @@ import {Link} from 'react-router-dom';
class Header extends Component {
renderContent() {
console.log('this.props.auth', this.props.auth);
switch (this.props.auth) {
case null:
return;
case false:
return (
<li>
<a href="/auth/google">Login With Google</a>
<a href="/api/auth/google">Login With Google</a>
</li>
);
default:
if (!this.props.auth.plexToken) {
return (
<li key="3" style={{margin: '0 10px'}}>
<Link to="/app/plex" className="waves-effect waves-light btn">
<i className="material-icons left">live_tv</i>Import Plex
</Link>
</li>
);
}
return (
<li key="2" style={{margin: '0 10px'}}>
<a href="/auth/logout">Logout</a>
<a href="/api/auth/logout">Logout</a>
</li>
);
}
@@ -35,7 +27,7 @@ class Header extends Component {
<nav>
<div className="nav-wrapper" style={{margin: '0 10px'}}>
<Link to={this.props.auth ? '/' : '/'} className="left brand-logo">
PlexRec
PlexRex
</Link>
<ul className="right">{this.renderContent()}</ul>
</div>

View File

@@ -0,0 +1,102 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import CssBaseline from '@material-ui/core/CssBaseline';
import Typography from '@material-ui/core/Typography';
import {withStyles} from '@material-ui/core/styles';
import {Link} from 'react-router-dom';
import {connect} from 'react-redux';
import styles from './css';
class Hero extends Component {
render() {
const {classes} = this.props;
if (this.props.auth) {
return (
<React.Fragment>
<CssBaseline />
<main>
<div className={classes.heroUnit}>
<div className={classes.heroContent}>
<Typography
component="h1"
variant="h2"
align="center"
color="textPrimary"
gutterBottom
>
PlexRex
</Typography>
<Typography
variant="h6"
align="center"
color="textSecondary"
paragraph
>
Media recommendations based on your most watched Plex TV and
Movies.
</Typography>
<div className="center-align">
<Link
to="/app/plex"
className="waves-effect waves-light btn-large"
>
<i className="material-icons left">live_tv</i>Sign In
</Link>
</div>
</div>
</div>
</main>
</React.Fragment>
);
}
return (
<React.Fragment>
<CssBaseline />
<main>
<div className={classes.heroUnit}>
<div className={classes.heroContent}>
<Typography
component="h1"
variant="h2"
align="center"
color="textPrimary"
gutterBottom
>
PlexRex
</Typography>
<Typography
variant="h6"
align="center"
color="textSecondary"
paragraph
>
Media recommendations based on your most watched Plex TV and
Movies.
</Typography>
<div className="center-align">
<a
href="/api/auth/google"
className="waves-effect waves-light btn-large"
>
<i className="material-icons left">live_tv</i> Login with
Google
</a>
</div>
</div>
</div>
</main>
</React.Fragment>
);
}
}
Hero.propTypes = {
classes: PropTypes.object.isRequired,
};
function mapStateToProps({auth}) {
return {auth};
}
export default connect(mapStateToProps)(withStyles(styles)(Hero));

View File

@@ -1,12 +0,0 @@
import React from 'react';
const Landing = () => {
return (
<div style={{textAlign: 'center'}}>
<h1>Emaily!</h1>
Collect feedback from your users
</div>
);
};
export default Landing;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import {withStyles} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import MuiDialogTitle from '@material-ui/core/DialogTitle';
import MuiDialogContent from '@material-ui/core/DialogContent';
import MuiDialogActions from '@material-ui/core/DialogActions';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Typography from '@material-ui/core/Typography';
const DialogTitle = withStyles(theme => ({
root: {
borderBottom: `1px solid ${theme.palette.divider}`,
margin: 0,
padding: theme.spacing.unit * 2,
},
closeButton: {
position: 'absolute',
right: theme.spacing.unit,
top: theme.spacing.unit,
color: theme.palette.grey[500],
},
}))(props => {
const {children, classes, onClose} = props;
return (
<MuiDialogTitle disableTypography className={classes.root}>
<Typography variant="h6">{children}</Typography>
{onClose ? (
<IconButton
aria-label="Close"
className={classes.closeButton}
onClick={onClose}
>
<CloseIcon />
</IconButton>
) : null}
</MuiDialogTitle>
);
});
const DialogContent = withStyles(theme => ({
root: {
margin: 0,
padding: theme.spacing.unit * 2,
},
}))(MuiDialogContent);
const DialogActions = withStyles(theme => ({
root: {
borderTop: `1px solid ${theme.palette.divider}`,
margin: 0,
padding: theme.spacing.unit,
},
}))(MuiDialogActions);
class CustomizedDialogDemo extends React.Component {
state = {
open: false,
};
handleClickOpen = () => {
this.setState({
open: true,
});
};
handleClose = () => {
this.setState({open: false});
};
render() {
return (
<div>
<button
className="btn-small waves-effect waves-light center-align"
onClick={this.handleClickOpen}
name="action"
>
<i className="material-icons right">info</i>
Info
</button>
<Dialog
onClose={this.handleClose}
aria-labelledby="customized-dialog-title"
open={this.state.open}
>
<DialogTitle id="customized-dialog-title" onClose={this.handleClose}>
What do we do with this info?
</DialogTitle>
<DialogContent>
<Typography gutterBottom>
We need to make a request to fetch a token which will allow us to
get a list of your most watched media. We will never store you
Plex password or make any changes to your plex server. This is an
open source project and you can view the code&nbsp;
<a href="https://github.com/mjrode/recommend"> here.</a>
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary">
Ok
</Button>
</DialogActions>
</Dialog>
</div>
);
}
}
export default CustomizedDialogDemo;

View File

@@ -0,0 +1,60 @@
const styles = theme => ({
appBar: {
position: 'relative',
},
icon: {
marginRight: theme.spacing.unit * 2,
},
heroUnit: {
backgroundColor: theme.palette.background.paper,
},
heroContent: {
maxWidth: 600,
margin: '0 auto',
padding: `${theme.spacing.unit * 8}px 0 ${theme.spacing.unit * 6}px`,
},
heroContentSmall: {
maxWidth: 600,
margin: '0 auto',
padding: `${theme.spacing.unit * 2}px 0 ${theme.spacing.unit * 1}px`,
},
heroButtons: {
marginTop: theme.spacing.unit * 4,
},
layout: {
width: 'auto',
marginLeft: theme.spacing.unit * 3,
marginRight: theme.spacing.unit * 3,
[theme.breakpoints.up(1100 + theme.spacing.unit * 3 * 2)]: {
width: 1100,
marginLeft: 'auto',
marginRight: 'auto',
},
},
cardGrid: {
padding: `${theme.spacing.unit * 8}px 0`,
},
card: {
height: '100%',
display: 'flex',
flexDirection: 'column',
},
cardMedia: {
paddingTop: '56.25%', // 16:9
},
cardContent: {
flexGrow: 1,
},
footer: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing.unit * 6,
},
shrinkTopMargin: {
margin: `${theme.spacing.unit}px ${theme.spacing.unit}px ${
theme.spacing.unit
}px ${theme.spacing.unit}px`,
},
});
export default styles;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import {connect} from 'react-redux';
import axios from 'axios';
class PlexForm extends React.Component {
state = {username: '', password: '', section_data: ''};
@@ -21,8 +21,8 @@ class PlexForm extends React.Component {
};
render() {
console.log(this.props.user);
if (this.props.user.plexToken) {
console.log(this.props.auth);
if (this.props.auth.plexToken) {
return (
<div className="input-field col s6">
<button
@@ -39,46 +39,52 @@ class PlexForm extends React.Component {
);
}
return (
<div className="row">
<form onSubmit={this.onFormSubmit} className="col s12">
<div className="row">
<div className="input-field col s6">
<i className="material-icons prefix">account_circle</i>
<input
id="username"
type="text"
className="validate"
value={this.state.username}
onChange={e => this.setState({username: e.target.value})}
/>
<label htmlFor="username">Plex Username</label>
<div className="section">
<div className="row">
<form onSubmit={this.onFormSubmit} className="col s12">
<div className="row">
<div className="input-field col s6">
<i className="material-icons prefix">account_circle</i>
<input
id="username"
type="text"
className="validate"
value={this.state.username}
onChange={e => this.setState({username: e.target.value})}
/>
<label htmlFor="username">Plex Username</label>
</div>
<div className="input-field col s6">
<i className="material-icons prefix">lock_open</i>
<input
id="password"
type="password"
className="validate"
value={this.state.password}
onChange={e => this.setState({password: e.target.value})}
/>
<label htmlFor="password">password</label>
</div>
</div>
<div className="input-field col s6">
<i className="material-icons prefix">lock_open</i>
<input
id="password"
type="password"
className="validate"
value={this.state.password}
onChange={e => this.setState({password: e.target.value})}
/>
<label htmlFor="password">password</label>
<button
className="btn waves-effect waves-light"
type="submit"
name="action"
>
Submit
<i className="material-icons right">send</i>
</button>
</div>
</div>
<div className="input-field col s6">
<button
className="btn waves-effect waves-light"
type="submit"
name="action"
>
Submit
<i className="material-icons right">send</i>
</button>
</div>
</form>
</form>
</div>
</div>
);
}
}
export default PlexForm;
function mapStateToProps({auth}) {
return {auth};
}
export default connect(mapStateToProps)(PlexForm);

View File

@@ -10,7 +10,11 @@ class PlexNew extends Component {
render() {
console.log(this.props.auth);
if (!this.props.auth.plexToken) {
return <PlexForm onSubmit={this.submit} />;
return (
<div class="valign-wrapper">
<PlexForm onSubmit={this.submit} />
</div>
);
}
return <PlexActions />;

View File

@@ -6,6 +6,7 @@ import {createStore, applyMiddleware} from 'redux';
import reduxThunk from 'redux-thunk';
import axios from 'axios';
import App from './components/App';
import reducers from './reducers';

View File

@@ -36,6 +36,7 @@ export default () => {
// Set up routes
server.use('/api/plex', plex);
server.use('/api/tdaw', tdaw);
server.use('/auth', auth);
server.use('/api/auth', auth);
if (process.env.NODE_ENV === 'production') {

View File

@@ -19,7 +19,7 @@ passport.use(
{
clientID: keys.server.googleClientID,
clientSecret: keys.server.googleClientSecret,
callbackURL: '/auth/google/callback',
callbackURL: '/api/auth/google/callback',
proxy: true,
},
async (accessToken, refreshToken, profile, done) => {