diff --git a/code/web/src/client/components/App.js b/code/web/src/client/components/App.js index 307772a88..64130a69d 100644 --- a/code/web/src/client/components/App.js +++ b/code/web/src/client/components/App.js @@ -6,12 +6,13 @@ import { Route, Switch } from 'react-router-dom' import { routes } from '../setup/routes' import Layout from './common/Layout' import NotFound from './common/NotFound' +import RoutePrivate from './user/RoutePrivate' const App = (props) => ( { Object.values(routes).map((route, index) => ( - + route.auth ? : )) } diff --git a/code/web/src/client/components/common/header/Header.js b/code/web/src/client/components/common/header/Header.js index 7c14d62f3..316737595 100644 --- a/code/web/src/client/components/common/header/Header.js +++ b/code/web/src/client/components/common/header/Header.js @@ -32,6 +32,8 @@ const Header = (props) => { Women How It Works + + What's New @@ -41,9 +43,9 @@ const Header = (props) => { props.user.isAuthenticated ? - Subscription + Subscription - Profile + Profile : diff --git a/code/web/src/client/components/home/Home.js b/code/web/src/client/components/home/Home.js index 91e358083..dfa386dbc 100644 --- a/code/web/src/client/components/home/Home.js +++ b/code/web/src/client/components/home/Home.js @@ -13,7 +13,7 @@ import { white } from '../ui/common/colors' import { textLevel1 } from '../ui/common/shadows' // App Imports -import user from '../../setup/routes/user' +import userRoutes from '../../setup/routes/user' // Component const Home = (props) => ( @@ -32,11 +32,11 @@ const Home = (props) => ( { props.user.isAuthenticated ? - + : - + } diff --git a/code/web/src/client/components/home/HowItWorks.js b/code/web/src/client/components/home/HowItWorks.js index cd8d5f40f..c7bba14fe 100644 --- a/code/web/src/client/components/home/HowItWorks.js +++ b/code/web/src/client/components/home/HowItWorks.js @@ -76,7 +76,7 @@ const HowItWorks = (props) => ( { props.user.isAuthenticated ? - + : diff --git a/code/web/src/client/components/home/Men.js b/code/web/src/client/components/home/Men.js index 7b75a635c..6203dbfe9 100644 --- a/code/web/src/client/components/home/Men.js +++ b/code/web/src/client/components/home/Men.js @@ -56,7 +56,7 @@ const Men = (props) => ( { props.user.isAuthenticated ? - + : diff --git a/code/web/src/client/components/home/WhatsNew.js b/code/web/src/client/components/home/WhatsNew.js new file mode 100644 index 000000000..04de8d97f --- /dev/null +++ b/code/web/src/client/components/home/WhatsNew.js @@ -0,0 +1,95 @@ +// Imports +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { Helmet } from 'react-helmet' +import { Link } from 'react-router-dom' + +// UI Imports +import { Grid, GridCell } from '../ui/grid' +import { H3, H4 } from '../ui/typography' +import Button from '../ui/button' +import Icon from '../ui/icon' +import { textLevel1 } from '../ui/common/shadows' +import { white, grey, grey3 } from '../ui/common/colors' + +// App Imports +import userRoutes from '../../setup/routes/user' +import { actionBlogsFetch } from './api/actions' + +// Component +class WhatsNew extends Component { + + static fetchData({store}) { + return store.dispatch(actionBlogsFetch()) + } + + componentDidMount() { + this.props.actionBlogsFetch() + } + + refresh() { + this.props.actionBlogsFetch() + } + + render() { + return ( +
+ {/* SEO */} + + What's new - Crate + + + {/* Top title bar */} + + +

What's new

+
+
+ + { + this.props.blogs.list.length > 0 + ? + this.props.blogs.list.map(blog => ( +

{ blog.title }

+ )) + : +

Loading...

+ } + + {/* Bottom call to action bar */} + + + { + this.props.user.isAuthenticated + ? + + + + : + + + + } + + +
+ ) + } +} + +// Component Properties +WhatsNew.propTypes = { + user: PropTypes.object.isRequired, + actionBlogsFetch: PropTypes.func.isRequired, +} + +// Component State +function whatsNewState(state) { + return { + user: state.user, + blogs: state.blogs + } +} + +export default connect(whatsNewState, { actionBlogsFetch })(WhatsNew) \ No newline at end of file diff --git a/code/web/src/client/components/home/Women.js b/code/web/src/client/components/home/Women.js index a2bd6ddb9..037f4fd9f 100644 --- a/code/web/src/client/components/home/Women.js +++ b/code/web/src/client/components/home/Women.js @@ -56,7 +56,7 @@ const Women = (props) => ( { props.user.isAuthenticated ? - + : diff --git a/code/web/src/client/components/home/api/actions.js b/code/web/src/client/components/home/api/actions.js new file mode 100644 index 000000000..b79bd0ffd --- /dev/null +++ b/code/web/src/client/components/home/api/actions.js @@ -0,0 +1,73 @@ +// Imports +import axios from 'axios' + +export const ACTION_TYPE_BLOGS_FETCH = 'ACTION_TYPE_BLOGS_FETCH' +export const ACTION_TYPE_BLOGS_FETCHING = 'ACTION_TYPE_BLOGS_FETCHING' +export const ACTION_TYPE_BLOG_FETCH = 'ACTION_TYPE_BLOG_FETCH' +export const ACTION_TYPE_BLOG_FETCHING = 'ACTION_TYPE_BLOG_FETCHING' + +export function actionBlogsFetch() { + return (dispatch, getState) => { + let state = getState() + + if(state.blogs.list.length === 0) { + dispatch({ + type: ACTION_TYPE_BLOGS_FETCHING + }) + + return axios.get('https://jsonplaceholder.typicode.com/posts') + .then((response) => { + if(response.status === 200) { + dispatch({ + type: ACTION_TYPE_BLOGS_FETCH, + blogs: response.data + }) + } else { + console.error(response) + } + }) + .catch(function (error) { + console.error(error) + }) + } + } +} + +export function actionBlogFetch({ id }) { + return (dispatch) => { + dispatch({ + type: ACTION_TYPE_BLOG_FETCHING + }) + + return axios.get(`https://jsonplaceholder.typicode.com/posts/${ id }`) + .then((response) => { + if(response.status === 200) { + dispatch({ + type: ACTION_TYPE_BLOG_FETCH, + blog: response.data + }) + } else { + console.error(response) + } + }) + .catch(function (error) { + console.error(error) + }) + } +} + +export const actionBlogFetchIfNeeded = ({ id }) => { + return (dispatch, getState) => { + let state = getState() + + if(typeof state.blog.details[id] === 'undefined') { + return dispatch(actionBlogFetch({ id })) + } + } +} + +export const actionBlogAdd = (blog) => { + return (dispatch) => { + return axios.post(`https://jsonplaceholder.typicode.com/posts`, blog) + } +} \ No newline at end of file diff --git a/code/web/src/client/components/home/api/state.js b/code/web/src/client/components/home/api/state.js new file mode 100644 index 000000000..03f9d3da6 --- /dev/null +++ b/code/web/src/client/components/home/api/state.js @@ -0,0 +1,27 @@ +// App Imports +import { + ACTION_TYPE_BLOGS_FETCH, + ACTION_TYPE_BLOGS_FETCHING +} from './actions' + +export default (state = { list: [], error: false, loading: false }, action = {}) => { + switch (action.type) { + + case ACTION_TYPE_BLOGS_FETCHING: + return Object.assign({}, state, { + list: [], + error: false, + loading: true + }) + + case ACTION_TYPE_BLOGS_FETCH: + return Object.assign({}, state, { + list: action.blogs, + error: action.error, + loading: false + }) + + default: + return state + } +} \ No newline at end of file diff --git a/code/web/src/client/components/user/Login.js b/code/web/src/client/components/user/Login.js index b9c2af265..6d9a45d9c 100644 --- a/code/web/src/client/components/user/Login.js +++ b/code/web/src/client/components/user/Login.js @@ -17,7 +17,6 @@ import { white } from '../ui/common/colors' // App Imports import userRoutes from '../../setup/routes/user' -import homeRoutes from '../../setup/routes/home' import { messageShow, messageHide } from '../common/api/actions' import { login } from './api/actions' import AuthCheck from './AuthCheck' diff --git a/code/web/src/client/components/user/Profile.js b/code/web/src/client/components/user/Profile.js new file mode 100644 index 000000000..6cb71d615 --- /dev/null +++ b/code/web/src/client/components/user/Profile.js @@ -0,0 +1,38 @@ +// Imports +import React from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import { Helmet } from 'react-helmet' +import { Link } from 'react-router-dom' + +// UI Imports +import { Grid, GridCell } from '../ui/grid' +import { H1, H4 } from '../ui/typography' +import Button from '../ui/button' +import { white } from '../ui/common/colors' +import { textLevel1 } from '../ui/common/shadows' + +// App Imports +import user from '../../setup/routes/user' +import { logout } from './api/actions' + +// Component +const Profile = (props) => ( + + {/* SEO */} + + Profile - Crate + + + + + + +) + +// Component Properties +Profile.propTypes = { + logout: PropTypes.func.isRequired +} + +export default connect(null, { logout })(Profile) diff --git a/code/web/src/client/components/user/RoutePrivate.js b/code/web/src/client/components/user/RoutePrivate.js new file mode 100644 index 000000000..022f7781c --- /dev/null +++ b/code/web/src/client/components/user/RoutePrivate.js @@ -0,0 +1,29 @@ +// Imports +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { Route, Redirect } from 'react-router-dom' +import { connect } from 'react-redux' + +// App Imports +import userRoutes from '../../setup/routes/user' + +// Component +class RoutePrivate extends Component { + render() { + const { isAuthenticated } = this.props.user + + return ( isAuthenticated ? : ) + } +} + +RoutePrivate.propTypes = { + user: PropTypes.object.isRequired, +} + +function mapStateToProps(state) { + return { + user: state.user + } +} + +export default connect(mapStateToProps, {})(RoutePrivate); diff --git a/code/web/src/client/components/user/Subscriptions.js b/code/web/src/client/components/user/Subscriptions.js new file mode 100644 index 000000000..3825a37a1 --- /dev/null +++ b/code/web/src/client/components/user/Subscriptions.js @@ -0,0 +1,13 @@ +// Imports +import React from 'react' + +// App Imports + +// Component +const Subscription = () => ( +
+

Subscription

+
+) + +export default Subscription \ No newline at end of file diff --git a/code/web/src/client/components/user/api/actions.js b/code/web/src/client/components/user/api/actions.js index 417335247..f518df5a6 100644 --- a/code/web/src/client/components/user/api/actions.js +++ b/code/web/src/client/components/user/api/actions.js @@ -61,14 +61,14 @@ export function login(userCredentials) { } } -// +// Set user token and info in localStorage and cookie export function loginSetUserLocalStorageAndCookie(token, user) { // Update token window.localStorage.setItem('token', token) window.localStorage.setItem('user', JSON.stringify(user)) // Set cookie for SSR - cookie.set('token', { token, user }, { path: '/' }) + cookie.set('auth', { token, user }, { path: '/' }) } // Register a user @@ -81,10 +81,20 @@ export function register(userDetails) { // Log out user and remove token from localStorage export function logout() { return dispatch => { - window.localStorage.removeItem('token') + logoutUnsetUserLocalStorageAndCookie() dispatch({ type: LOGOUT }) } } + +// Unset user token and info in localStorage and cookie +export function logoutUnsetUserLocalStorageAndCookie(token, user) { + // Update token + window.localStorage.removeItem('token') + window.localStorage.removeItem('user') + + // Set cookie for SSR + cookie.remove('auth') +} \ No newline at end of file diff --git a/code/web/src/client/setup/routes/home.js b/code/web/src/client/setup/routes/home.js index a9b658634..2bd9b6287 100644 --- a/code/web/src/client/setup/routes/home.js +++ b/code/web/src/client/setup/routes/home.js @@ -3,6 +3,7 @@ import Home from '../../components/home/Home' import Men from '../../components/home/Men' import Women from '../../components/home/Women' import HowItWorks from '../../components/home/HowItWorks' +import WhatsNew from '../../components/home/WhatsNew' // Home routes export default { @@ -25,5 +26,10 @@ export default { howItWorks: { path: '/how-it-works', component: HowItWorks + }, + + whatsNew: { + path: '/whats-new', + component: WhatsNew } } \ No newline at end of file diff --git a/code/web/src/client/setup/routes/user.js b/code/web/src/client/setup/routes/user.js index 9df86b225..44cd722f9 100644 --- a/code/web/src/client/setup/routes/user.js +++ b/code/web/src/client/setup/routes/user.js @@ -1,6 +1,8 @@ // App Imports import Login from '../../components/user/Login' import Signup from '../../components/user/Signup' +import Profile from '../../components/user/Profile' +import Subscriptions from '../../components/user/Subscriptions' // User routes export default { @@ -12,5 +14,17 @@ export default { signup: { path: '/user/signup', component: Signup + }, + + profile: { + path: '/user/profile', + component: Profile, + auth: true + }, + + subscriptions: { + path: '/user/subscriptions', + component: Subscriptions, + auth: true } } diff --git a/code/web/src/client/setup/store.js b/code/web/src/client/setup/store.js index 8da7894df..21b2a3362 100644 --- a/code/web/src/client/setup/store.js +++ b/code/web/src/client/setup/store.js @@ -6,11 +6,13 @@ import thunk from 'redux-thunk' // App Imports import common from '../components/common/api/state' import user from '../components/user/api/state' +import blogs from '../components/home/api/state' // App Reducer const appReducer = combineReducers({ common, - user + user, + blogs }) // Root Reducer diff --git a/code/web/src/server/index.js b/code/web/src/server/index.js index 37472b55d..c82116515 100644 --- a/code/web/src/server/index.js +++ b/code/web/src/server/index.js @@ -45,8 +45,8 @@ const store = createStore( app.get('*', (request, response) => { // Check for auth - if(request.cookies.token) { - const auth = JSON.parse(request.cookies.token) + if(request.cookies.auth) { + const auth = JSON.parse(request.cookies.auth) if (auth && auth.token !== '' && auth.user) { store.dispatch(setUser(auth.token, auth.user))