35
client/app/components/Admin.jsx
Normal file
35
client/app/components/Admin.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
./app/components/App.jsx
|
||||
|
||||
<FetchData url={"https://apollo.kevinmidboe.com/api/v1/plex/playing"} />
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import FetchData from './FetchData.js';
|
||||
import ListStrays from './ListStrays.jsx';
|
||||
|
||||
import FetchRequested from './FetchRequested.jsx';
|
||||
|
||||
import LoginForm from './LoginForm/LoginForm.jsx';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './redux/store.jsx';
|
||||
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
|
||||
function getLoginStatus() {
|
||||
const logged_in = getCookie('logged_in');
|
||||
if (logged_in) {
|
||||
return <FetchRequested />
|
||||
}
|
||||
return <LoginForm />
|
||||
}
|
||||
|
||||
const Admin = () => (
|
||||
<Provider store={store}>
|
||||
{ getLoginStatus() }
|
||||
</Provider>
|
||||
)
|
||||
|
||||
export default Admin
|
||||
@@ -1,27 +1,12 @@
|
||||
/*
|
||||
./app/components/App.jsx
|
||||
|
||||
<FetchData url={"https://apollo.kevinmidboe.com/api/v1/plex/playing"} />
|
||||
*/
|
||||
import React from 'react';
|
||||
import FetchData from './FetchData.js';
|
||||
import ListStrays from './ListStrays.jsx'
|
||||
import SearchRequest from './SearchRequest.jsx';
|
||||
import React, { Component } from "react";
|
||||
import Header from './Header.jsx';
|
||||
import Main from './Main.jsx';
|
||||
|
||||
export default class App extends React.Component {
|
||||
const App = () => (
|
||||
<div>
|
||||
<Header />
|
||||
<Main />
|
||||
</div>
|
||||
)
|
||||
|
||||
// <div>
|
||||
// <h1>Welcome to Seasoned</h1>
|
||||
// </div>
|
||||
// <ListStrays />
|
||||
|
||||
// <FetchData />
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
|
||||
<SearchRequest />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default App
|
||||
26
client/app/components/Cookie.jsx
Normal file
26
client/app/components/Cookie.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for(var i = 0; i <ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays*24*60*60*1000));
|
||||
var expires = "expires="+ d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
165
client/app/components/FetchRequested.jsx
Normal file
165
client/app/components/FetchRequested.jsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
|
||||
import requestElement from './styles/requestElementStyle.jsx'
|
||||
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
class RequestElement extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.default_requestList = null;
|
||||
}
|
||||
|
||||
filterRequestList(requestList, filter) {
|
||||
if (filter === 'all')
|
||||
return requestList
|
||||
|
||||
return requestList.filter(item => item.status === filter)
|
||||
}
|
||||
|
||||
sortRequestList(requestList, sort_type, reversed) {
|
||||
requestList.sort(function(a,b) {
|
||||
if(a[sort_type] < b[sort_type]) return -1;
|
||||
if(a[sort_type] > b[sort_type]) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (reversed)
|
||||
requestList.reverse();
|
||||
}
|
||||
|
||||
createHTMLElement(data, index) {
|
||||
var posterPath = 'https://image.tmdb.org/t/p/w300' + data.image_path;
|
||||
|
||||
if (data.user_agent !== null) {
|
||||
var user_agent = data.user_agent.split(" ");
|
||||
var agent_shortened = user_agent[1].replace(/[\(\;]/g, '')
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={requestElement.wrappingDiv} key={index}>
|
||||
<img style={requestElement.requestPoster} src={posterPath}></img>
|
||||
<div style={requestElement.infoDiv}>
|
||||
<span><b>Name</b>: {data.name} </span>
|
||||
<span><b>Year</b>: {data.year}</span><br></br>
|
||||
<span><b>Status</b>: {data.status}</span><br></br>
|
||||
<span><b>Address</b>: {data.ip}</span><br></br>
|
||||
<span><b>Requested Data:</b> {data.requested_date}</span><br></br>
|
||||
<span><b>Requested By:</b> {data.requested_by}</span><br></br>
|
||||
<span><b>Agent</b>: {agent_shortened}</span><br></br>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {requestedElementsList, requestedElementsFilter, requestedElementsSort, props} = this.props;
|
||||
|
||||
var filteredRequestedList = this.filterRequestList(requestedElementsList, requestedElementsFilter)
|
||||
|
||||
this.sortRequestList(filteredRequestedList, requestedElementsSort.value, requestedElementsSort.reversed)
|
||||
|
||||
return (
|
||||
<div {...props} style={requestElement.bodyDiv}>
|
||||
{filteredRequestedList.map((requestItem, index) => this.createHTMLElement(requestItem, index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FetchRequested extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
requested_objects: [],
|
||||
filter: 'all',
|
||||
sort: {
|
||||
value: 'requested_date',
|
||||
reversed: false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
'authorization': getCookie('token')
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status !== 200) {
|
||||
console.log('error');
|
||||
}
|
||||
|
||||
response.json()
|
||||
.then(data => {
|
||||
if (data.success === true) {
|
||||
this.setState({
|
||||
requested_objects: data.requestedItems
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
new Error(error);
|
||||
})
|
||||
}
|
||||
|
||||
changeFilter(filter) {
|
||||
this.setState({
|
||||
filter: filter
|
||||
})
|
||||
}
|
||||
|
||||
updateSort(sort=null, reverse=false) {
|
||||
if (sort) {
|
||||
this.setState({
|
||||
sort: { value: sort, reversed: reverse }
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
sort: { value: this.state.sort.value, reversed: reverse }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<select id="lang" onChange={event => this.changeFilter(event.target.value)} value={this.state.value}>
|
||||
<option value="all">All</option>
|
||||
<option value="requested">Requested</option>
|
||||
<option value="downloading">Downloading</option>
|
||||
<option value="downloaded">Downloaded</option>
|
||||
</select>
|
||||
|
||||
<select id="lang" onChange={event => this.updateSort(event.target.value)} value={this.state.value}>
|
||||
<option value='requested_date'>Date</option>
|
||||
<option value='name'>Title</option>
|
||||
<option value='status'>Status</option>
|
||||
<option value='requested_by'>Requested By</option>
|
||||
<option value='ip'>Address</option>
|
||||
<option value='user_agent'>Agent</option>
|
||||
</select>
|
||||
|
||||
<button onClick={() => {this.updateSort(null, !this.state.sort.reversed)}}>Reverse</button>
|
||||
|
||||
<RequestElement
|
||||
requestedElementsList={this.state.requested_objects}
|
||||
requestedElementsFilter={this.state.filter}
|
||||
requestedElementsSort={this.state.sort}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FetchRequested;
|
||||
11
client/app/components/Header.jsx
Normal file
11
client/app/components/Header.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
// The Header creates links that can be used to navigate
|
||||
// between routes.
|
||||
const Header = () => (
|
||||
<header>
|
||||
</header>
|
||||
)
|
||||
|
||||
export default Header
|
||||
66
client/app/components/LoginForm/LoginForm.jsx
Normal file
66
client/app/components/LoginForm/LoginForm.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { login } from '../redux/reducer.jsx';
|
||||
|
||||
class LoginForm extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {email, password} = this.state;
|
||||
let {isLoginPending, isLoginSuccess, loginError} = this.props;
|
||||
return (
|
||||
<form name="loginForm" onSubmit={this.onSubmit}>
|
||||
<div className="form-group-collection">
|
||||
<div className="form-group">
|
||||
<label>Email:</label>
|
||||
<input type="" name="email" onChange={e => this.setState({email: e.target.value})} value={email}/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" onChange={e => this.setState({password: e.target.value})} value={password}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Login" />
|
||||
|
||||
<div className="message">
|
||||
{ isLoginPending && <div>Please wait...</div> }
|
||||
{ isLoginSuccess && <div>Success.</div> }
|
||||
{ loginError && <div>{loginError.message}</div> }
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
onSubmit(e) {
|
||||
e.preventDefault();
|
||||
let { email, password } = this.state;
|
||||
this.props.login(email, password);
|
||||
this.setState({
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
isLoginPending: state.isLoginPending,
|
||||
isLoginSuccess: state.isLoginSuccess,
|
||||
loginError: state.loginError
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
login: (email, password) => dispatch(login(email, password))
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
|
||||
21
client/app/components/Main.jsx
Normal file
21
client/app/components/Main.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { HashRouter as Router, Route, Switch} from 'react-router-dom';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import SearchRequest from './SearchRequest.jsx';
|
||||
import Admin from './Admin.jsx';
|
||||
import NotFound from './NotFound.js';
|
||||
|
||||
export const history = createBrowserHistory();
|
||||
|
||||
const Main = () => (
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route exact path='/' component={SearchRequest} />
|
||||
<Route path='/admin' component={Admin} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</Router>
|
||||
)
|
||||
|
||||
export default Main
|
||||
@@ -34,7 +34,7 @@ class MovieObject {
|
||||
notify.show(this.title + ' requested!', 'success', 3000);
|
||||
}
|
||||
|
||||
getElement() {
|
||||
getElement(index) {
|
||||
// TODO set the poster image async by updating the dom after this is returned
|
||||
if (this.poster == null || this.poster == undefined) {
|
||||
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
|
||||
@@ -60,7 +60,7 @@ class MovieObject {
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div key={index}>
|
||||
<Notifications />
|
||||
<div style={movieStyle.resultItem} key={this.id}>
|
||||
<MediaQuery minWidth={600}>
|
||||
|
||||
10
client/app/components/NotFound.js
Normal file
10
client/app/components/NotFound.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// components/NotFound.js
|
||||
import React from 'react';
|
||||
|
||||
const NotFound = () =>
|
||||
<div>
|
||||
<h3>404 page not found</h3>
|
||||
<p>We are sorry but the page you are looking for does not exist.</p>
|
||||
</div>
|
||||
|
||||
export default NotFound;
|
||||
@@ -156,11 +156,11 @@ class SearchRequest extends React.Component {
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
this.setState({
|
||||
responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)),
|
||||
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
} else {
|
||||
let responseMovieObjects = responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem));
|
||||
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
|
||||
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
|
||||
this.setState({
|
||||
responseMovieList: growingReponseMovieObjectList,
|
||||
@@ -202,11 +202,11 @@ class SearchRequest extends React.Component {
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
this.setState({
|
||||
responseMovieList: responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem)),
|
||||
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
} else {
|
||||
let responseMovieObjects = responseData.results.map(searchResultItem => this.createMovieObjects(searchResultItem));
|
||||
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
|
||||
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
|
||||
this.setState({
|
||||
responseMovieList: growingReponseMovieObjectList,
|
||||
@@ -288,9 +288,9 @@ class SearchRequest extends React.Component {
|
||||
|
||||
// When called passes the variable to MovieObject and calls it's interal function for
|
||||
// generating the wanted HTML
|
||||
createMovieObjects(item) {
|
||||
createMovieObjects(item, index) {
|
||||
let movie = new MovieObject(item);
|
||||
return movie.getElement();
|
||||
return movie.getElement(index);
|
||||
}
|
||||
|
||||
toggleFilter(filterType) {
|
||||
|
||||
108
client/app/components/redux/reducer.jsx
Normal file
108
client/app/components/redux/reducer.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
import { setCookie } from '../Cookie.jsx';
|
||||
|
||||
const SET_LOGIN_PENDING = 'SET_LOGIN_PENDING';
|
||||
const SET_LOGIN_SUCCESS = 'SET_LOGIN_SUCCESS';
|
||||
const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR';
|
||||
|
||||
export function login(email, password) {
|
||||
return dispatch => {
|
||||
dispatch(setLoginPending(true));
|
||||
dispatch(setLoginSuccess(false));
|
||||
dispatch(setLoginError(null));
|
||||
|
||||
callLoginApi(email, password, error => {
|
||||
dispatch(setLoginPending(false));
|
||||
if (!error) {
|
||||
dispatch(setLoginSuccess(true));
|
||||
} else {
|
||||
dispatch(setLoginError(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginPending(isLoginPending) {
|
||||
return {
|
||||
type: SET_LOGIN_PENDING,
|
||||
isLoginPending
|
||||
};
|
||||
}
|
||||
|
||||
function setLoginSuccess(isLoginSuccess) {
|
||||
return {
|
||||
type: SET_LOGIN_SUCCESS,
|
||||
isLoginSuccess
|
||||
};
|
||||
}
|
||||
|
||||
function setLoginError(loginError) {
|
||||
return {
|
||||
type: SET_LOGIN_ERROR,
|
||||
loginError
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function callLoginApi(email, password, callback) {
|
||||
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/user/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: email,
|
||||
password: password,
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
response.json()
|
||||
.then((data) => {
|
||||
if (data.success === true) {
|
||||
let token = data.token;
|
||||
setCookie('token', token, 10);
|
||||
setCookie('logged_in', true, 10);
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
return callback(null);
|
||||
})
|
||||
|
||||
case 401:
|
||||
return callback(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
return callback(new Error('Invalid email and password'));
|
||||
});
|
||||
}
|
||||
|
||||
export default function reducer(state = {
|
||||
isLoginSuccess: false,
|
||||
isLoginPending: false,
|
||||
loginError: null
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case SET_LOGIN_PENDING:
|
||||
return Object.assign({}, state, {
|
||||
isLoginPending: action.isLoginPending
|
||||
});
|
||||
|
||||
case SET_LOGIN_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isLoginSuccess: action.isLoginSuccess
|
||||
});
|
||||
|
||||
case SET_LOGIN_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
loginError: action.loginError
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
7
client/app/components/redux/store.jsx
Normal file
7
client/app/components/redux/store.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import logger from 'redux-logger';
|
||||
import reducer from './reducer.jsx';
|
||||
|
||||
const store = createStore(reducer, {}, applyMiddleware(thunk, logger));
|
||||
export default store;
|
||||
23
client/app/components/styles/requestElementStyle.jsx
Normal file
23
client/app/components/styles/requestElementStyle.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
|
||||
export default {
|
||||
bodyDiv: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexFlow: 'row wrap',
|
||||
},
|
||||
|
||||
wrappingDiv: {
|
||||
|
||||
},
|
||||
|
||||
requestPoster: {
|
||||
height: '150px',
|
||||
},
|
||||
|
||||
infoDiv: {
|
||||
marginTop: 0,
|
||||
marginLeft: '10px',
|
||||
float: 'right',
|
||||
},
|
||||
}
|
||||
@@ -2,14 +2,19 @@
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 21:08:55
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-06-01 21:34:32
|
||||
* @Last Modified time: 2017-10-05 13:47:37
|
||||
|
||||
./client/index.js
|
||||
which is the webpack entry file
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { render } from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import App from './components/App.jsx';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
render((
|
||||
<HashRouter>
|
||||
<App />
|
||||
</HashRouter>
|
||||
), document.getElementById('root'));
|
||||
@@ -17,7 +17,13 @@
|
||||
"react-dom": "^15.5.4",
|
||||
"react-infinite-scroller": "^1.0.15",
|
||||
"react-notify-toast": "^0.3.2",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-responsive": "^1.3.4",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux": "^3.7.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"urijs": "^1.18.12",
|
||||
"webfontloader": "^1.6.28",
|
||||
"webpack": "^3.5.5",
|
||||
|
||||
Reference in New Issue
Block a user