Merge pull request #41 from KevinMidboe/client/admin

Client/admin
This commit is contained in:
2017-10-06 12:28:05 +02:00
committed by GitHub
15 changed files with 504 additions and 36 deletions

View 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

View File

@@ -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

View 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=/";
}

View 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;

View 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

View 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);

View 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

View File

@@ -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}>

View 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;

View File

@@ -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) {

View 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;
}
}

View 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;

View 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',
},
}

View File

@@ -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'));

View File

@@ -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",