diff --git a/client/app/components/Admin.jsx b/client/app/components/Admin.jsx new file mode 100644 index 0000000..96d36af --- /dev/null +++ b/client/app/components/Admin.jsx @@ -0,0 +1,35 @@ +/* + ./app/components/App.jsx + + +*/ +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 + } + return +} + +const Admin = () => ( + + { getLoginStatus() } + +) + +export default Admin \ No newline at end of file diff --git a/client/app/components/App.jsx b/client/app/components/App.jsx index edc567c..6f87df6 100644 --- a/client/app/components/App.jsx +++ b/client/app/components/App.jsx @@ -1,27 +1,12 @@ -/* - ./app/components/App.jsx - - -*/ -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 = () => ( +
+
+
+
+) - //
- //

Welcome to Seasoned

- //
- // - - // - render() { - return ( -
- - -
- ); - } -} \ No newline at end of file +export default App \ No newline at end of file diff --git a/client/app/components/Cookie.jsx b/client/app/components/Cookie.jsx new file mode 100644 index 0000000..8aef20f --- /dev/null +++ b/client/app/components/Cookie.jsx @@ -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 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 ( +
+ +
+ Name: {data.name} + Year: {data.year}

+ Status: {data.status}

+ Address: {data.ip}

+ Requested Data: {data.requested_date}

+ Requested By: {data.requested_by}

+ Agent: {agent_shortened}

+
+
+ ) + } + + render() { + const {requestedElementsList, requestedElementsFilter, requestedElementsSort, props} = this.props; + + var filteredRequestedList = this.filterRequestList(requestedElementsList, requestedElementsFilter) + + this.sortRequestList(filteredRequestedList, requestedElementsSort.value, requestedElementsSort.reversed) + + return ( +
+ {filteredRequestedList.map((requestItem, index) => this.createHTMLElement(requestItem, index))} +
+ ); + } +} + + +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( +
+ + + + + + + + +
+ ) + } +} + +export default FetchRequested; \ No newline at end of file diff --git a/client/app/components/Header.jsx b/client/app/components/Header.jsx new file mode 100644 index 0000000..e068151 --- /dev/null +++ b/client/app/components/Header.jsx @@ -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 = () => ( +
+
+) + +export default Header diff --git a/client/app/components/LoginForm/LoginForm.jsx b/client/app/components/LoginForm/LoginForm.jsx new file mode 100644 index 0000000..f25ab83 --- /dev/null +++ b/client/app/components/LoginForm/LoginForm.jsx @@ -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 ( +
+
+
+ + this.setState({email: e.target.value})} value={email}/> +
+ +
+ + this.setState({password: e.target.value})} value={password}/> +
+
+ + + +
+ { isLoginPending &&
Please wait...
} + { isLoginSuccess &&
Success.
} + { loginError &&
{loginError.message}
} +
+
+ ) + } + + 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); \ No newline at end of file diff --git a/client/app/components/Main.jsx b/client/app/components/Main.jsx new file mode 100644 index 0000000..118bb50 --- /dev/null +++ b/client/app/components/Main.jsx @@ -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 = () => ( + + + + + + + +) + +export default Main diff --git a/client/app/components/MovieObject.jsx b/client/app/components/MovieObject.jsx index 5b6f58a..e114900 100644 --- a/client/app/components/MovieObject.jsx +++ b/client/app/components/MovieObject.jsx @@ -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 ( -
+
diff --git a/client/app/components/NotFound.js b/client/app/components/NotFound.js new file mode 100644 index 0000000..5984e9b --- /dev/null +++ b/client/app/components/NotFound.js @@ -0,0 +1,10 @@ +// components/NotFound.js +import React from 'react'; + +const NotFound = () => +
+

404 page not found

+

We are sorry but the page you are looking for does not exist.

+
+ +export default NotFound; \ No newline at end of file diff --git a/client/app/components/SearchRequest.jsx b/client/app/components/SearchRequest.jsx index 1f488dc..c9f64ca 100644 --- a/client/app/components/SearchRequest.jsx +++ b/client/app/components/SearchRequest.jsx @@ -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) { diff --git a/client/app/components/redux/reducer.jsx b/client/app/components/redux/reducer.jsx new file mode 100644 index 0000000..18317d7 --- /dev/null +++ b/client/app/components/redux/reducer.jsx @@ -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; + } +} \ No newline at end of file diff --git a/client/app/components/redux/store.jsx b/client/app/components/redux/store.jsx new file mode 100644 index 0000000..6af9d92 --- /dev/null +++ b/client/app/components/redux/store.jsx @@ -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; \ No newline at end of file diff --git a/client/app/components/styles/requestElementStyle.jsx b/client/app/components/styles/requestElementStyle.jsx new file mode 100644 index 0000000..8fa0c15 --- /dev/null +++ b/client/app/components/styles/requestElementStyle.jsx @@ -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', + }, +} \ No newline at end of file diff --git a/client/app/index.js b/client/app/index.js index ecc68f9..214f861 100644 --- a/client/app/index.js +++ b/client/app/index.js @@ -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(, document.getElementById('root')); \ No newline at end of file +render(( + + + +), document.getElementById('root')); \ No newline at end of file diff --git a/client/package.json b/client/package.json index e6fe23f..38c7c71 100644 --- a/client/package.json +++ b/client/package.json @@ -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",