diff --git a/lab-nathan/backend b/lab-nathan/backend
new file mode 160000
index 0000000..1fc6e99
--- /dev/null
+++ b/lab-nathan/backend
@@ -0,0 +1 @@
+Subproject commit 1fc6e992ae12d84d6e3f8dfc67a18c9f2eca29ad
diff --git a/lab-nathan/frontend/.babelrc b/lab-nathan/frontend/.babelrc
new file mode 100644
index 0000000..cf6ae40
--- /dev/null
+++ b/lab-nathan/frontend/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["es2015", "react"],
+ "plugins": ["transform-object-rest-spread"]
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/.gitignore b/lab-nathan/frontend/.gitignore
new file mode 100644
index 0000000..60a5626
--- /dev/null
+++ b/lab-nathan/frontend/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+coverage
+build
+.env
\ No newline at end of file
diff --git a/lab-nathan/frontend/package.json b/lab-nathan/frontend/package.json
new file mode 100644
index 0000000..4e62764
--- /dev/null
+++ b/lab-nathan/frontend/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "frontend",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "webpack",
+ "watch": "webpack-dev-server --inline --hot",
+ "test": "jest"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.2",
+ "babel-plugin-transform-object-rest-spread": "^6.26.0",
+ "babel-preset-es2015": "^6.24.1",
+ "babel-preset-react": "^6.24.1",
+ "clean-webpack-plugin": "^0.1.16",
+ "css-loader": "^0.28.7",
+ "dotenv": "^4.0.0",
+ "extract-text-webpack-plugin": "^3.0.0",
+ "file-loader": "^0.11.2",
+ "html-webpack-plugin": "^2.30.1",
+ "jest": "^21.0.1",
+ "node-sass": "^4.5.3",
+ "raw-loader": "^0.5.1",
+ "react": "^15.6.1",
+ "react-dom": "^15.6.1",
+ "react-redux": "^5.0.6",
+ "react-router": "^4.2.0",
+ "react-router-dom": "^4.2.2",
+ "redux": "^3.7.2",
+ "redux-mock-store": "^1.2.3",
+ "sass-loader": "^6.0.6",
+ "superagent": "^3.6.0",
+ "uglifyjs-webpack-plugin": "^0.4.6",
+ "url-loader": "^0.5.9",
+ "uuid": "^3.1.0",
+ "webpack": "^3.5.6",
+ "webpack-dev-server": "^2.7.1"
+ }
+}
diff --git a/lab-nathan/frontend/src/__test__/token-reducer.test.js b/lab-nathan/frontend/src/__test__/token-reducer.test.js
new file mode 100644
index 0000000..0e0ca69
--- /dev/null
+++ b/lab-nathan/frontend/src/__test__/token-reducer.test.js
@@ -0,0 +1,24 @@
+import tokenReducer from '../reducers/token-reducer.js';
+
+describe('Token Reducer', function() {
+ it('TOKEN_SET should return a token.', function() {
+ let testAction = {
+ type: 'TOKEN_SET',
+ payload: 'kasdf;kasdfj;asdfj;s'
+ };
+
+ let result = tokenReducer(null, testAction);
+
+ expect(result).toEqual(testAction.payload);
+ });
+
+ it('TOKEN_DELETE should delete a token.', function() {
+ let testAction = {
+ type: 'TOKEN_DELETE'
+ };
+
+ let result = tokenReducer(null, testAction);
+
+ expect(result).toEqual(null);
+ })
+});
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/actions/photo-actions.js b/lab-nathan/frontend/src/actions/photo-actions.js
new file mode 100644
index 0000000..367e78b
--- /dev/null
+++ b/lab-nathan/frontend/src/actions/photo-actions.js
@@ -0,0 +1,67 @@
+import superagent from 'superagent'
+
+// sync actions
+export const userPhotosSet = (photos) => ({
+ type: 'USER_PHOTOS_SET',
+ payload: photos,
+})
+
+export const userPhotoCreate = (photo) => ({
+ type: 'USER_PHOTO_CREATE',
+ payload: photo,
+})
+
+export const userPhotoUpdate = (photo) => ({
+ type: 'USER_PHOTO_UPDATE',
+ payload: photo,
+})
+
+export const userPhotoDelete = (photo) => ({
+ type: 'USER_PHOTO_DELETE',
+ payload: photo,
+})
+
+// async actions
+export const userPhotosFetchRequest = () => (dispatch, getState) => {
+ let {token} = getState()
+ return superagent.get(`${__API_URL__}/photos/me`)
+ .set('Authorization', `Bearer ${token}`)
+ .then(res => {
+ dispatch(userPhotosSet(res.body.data))
+ return res
+ })
+}
+
+export const userPhotoCreateRequest = (photo) => (dispatch, getState) => {
+ let {token} = getState()
+ return superagent.post(`${__API_URL__}/photos`)
+ .set('Authorization', `Bearer ${token}`)
+ .field('description', photo.description)
+ .attach('photo', photo.photo)
+ .then((res) => {
+ dispatch(userPhotoCreate(res.body))
+ return res
+ })
+}
+
+export const userPhotoDeleteRequest = (photo) => (dispatch, getState) => {
+ let {token} = getState()
+ return superagent.delete(`${__API_URL__}/photos/${photo._id}`)
+ .set('Authorization', `Bearer ${token}`)
+ .then(res => {
+ dispatch(userPhotoDelete(photo))
+ return res
+ })
+}
+
+export const userPhotoUpdateRequest = (photo) => (dispatch, getState) => {
+ let {token} = getState()
+ return superagent.put(`${__API_URL__}/photos/${photo._id}`)
+ .set('Authorization', `Bearer ${token}`)
+ .field('description', photo.description)
+ .attach('photo', photo.photo)
+ .then(res => {
+ dispatch(userPhotoUpdate(res.body))
+ return res
+ })
+}
diff --git a/lab-nathan/frontend/src/actions/profile-actions.js b/lab-nathan/frontend/src/actions/profile-actions.js
new file mode 100644
index 0000000..06409cf
--- /dev/null
+++ b/lab-nathan/frontend/src/actions/profile-actions.js
@@ -0,0 +1,48 @@
+import superagent from 'superagent'
+
+// sync action creators
+export const profileCreate = (profile) => ({
+ type: 'PROFILE_CREATE',
+ payload: profile,
+})
+
+export const profileUpdate = (profile) => ({
+ type: 'PROFILE_UPDATE',
+ payload: profile,
+})
+
+// async action creators
+export const profileCreateRequest = (profile) => (dispatch, getState) => {
+ let {token} = getState()
+ return superagent.post(`${__API_URL__}/profiles`)
+ .set('Authorization', `Bearer ${token}`)
+ .field('bio', profile.bio)
+ .attach('avatar', profile.avatar)
+ .then(res => {
+ dispatch(profileCreate(res.body))
+ return res
+ })
+}
+
+export const profileUpdateRequest = (profile) => (dispatch, getState) => {
+ let {token} = getState()
+ return superagent.put(`${__API_URL__}/profiles/${profile._id}`)
+ .set('Authorization', `Bearer ${token}`)
+ .field('bio', profile.bio)
+ .attach('avatar', profile.avatar)
+ .then(res => {
+ dispatch(profileCreate(res.body))
+ return res
+ })
+}
+
+export const profileFetchRequest = () => (dispatch, getState) => {
+ let {token} = getState()
+ return superagent.get(`${__API_URL__}/profiles/me`)
+ .set('Authorization', `Bearer ${token}`)
+ .then(res => {
+ console.log(res);
+ dispatch(profileCreate(res.body))
+ return res
+ })
+}
diff --git a/lab-nathan/frontend/src/actions/token-actions.js b/lab-nathan/frontend/src/actions/token-actions.js
new file mode 100644
index 0000000..a81198b
--- /dev/null
+++ b/lab-nathan/frontend/src/actions/token-actions.js
@@ -0,0 +1,43 @@
+import superagent from 'superagent';
+import * as util from '../lib/utilities.js';
+
+export const tokenSet = (token) => ({
+ type: 'TOKEN_SET',
+ payload: token
+});
+
+export const tokenDelete = (token) => ({
+ type: 'TOKEN_DELETE'
+});
+
+export const logout = () => {
+ util.deleteCookie('X-Sluggram-Token')
+ return { type: 'LOGOUT' }
+}
+
+export const signupRequest = (user) => (dispatch) => {
+ return superagent.post(`${__API_URL__}/signup`)
+ .withCredentials()
+ .send(user)
+ .then(response => {
+ dispatch(tokenSet(response.text));
+ try {
+ localStorage.token = response.text;
+ }
+ catch (error) {
+ console.log(error);
+ }
+
+ return response;
+ });
+};
+
+export const loginRequest = (user) => (dispatch) => {
+ return superagent.get(`${__API_URL__}/login`)
+ .withCredentials()
+ .auth(user.username, user.password)
+ .then(response => {
+ dispatch(tokenSet(response.text))
+ return response;
+ });
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_disabled_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_disabled_ios.svg
new file mode 100644
index 0000000..ecda263
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_dark_disabled_ios.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_focus_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_focus_ios.svg
new file mode 100644
index 0000000..dbf2c96
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_dark_focus_ios.svg
@@ -0,0 +1,51 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_normal_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_normal_ios.svg
new file mode 100644
index 0000000..8464cb2
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_dark_normal_ios.svg
@@ -0,0 +1,50 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_dark_pressed_ios.svg b/lab-nathan/frontend/src/assets/btn_google_dark_pressed_ios.svg
new file mode 100644
index 0000000..3552b43
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_dark_pressed_ios.svg
@@ -0,0 +1,50 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_light_disabled_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_disabled_ios.svg
new file mode 100644
index 0000000..b433d04
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_light_disabled_ios.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_light_focus_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_focus_ios.svg
new file mode 100644
index 0000000..1f3ee4f
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_light_focus_ios.svg
@@ -0,0 +1,44 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_light_normal_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_normal_ios.svg
new file mode 100644
index 0000000..032b6ac
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_light_normal_ios.svg
@@ -0,0 +1,43 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/btn_google_light_pressed_ios.svg b/lab-nathan/frontend/src/assets/btn_google_light_pressed_ios.svg
new file mode 100644
index 0000000..ee593d7
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/btn_google_light_pressed_ios.svg
@@ -0,0 +1,43 @@
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/assets/camera-icon.svg b/lab-nathan/frontend/src/assets/camera-icon.svg
new file mode 100644
index 0000000..2538ac2
--- /dev/null
+++ b/lab-nathan/frontend/src/assets/camera-icon.svg
@@ -0,0 +1,180 @@
+
+
+
diff --git a/lab-nathan/frontend/src/assets/noise.png b/lab-nathan/frontend/src/assets/noise.png
new file mode 100644
index 0000000..dd37251
Binary files /dev/null and b/lab-nathan/frontend/src/assets/noise.png differ
diff --git a/lab-nathan/frontend/src/components/app/_app.scss b/lab-nathan/frontend/src/components/app/_app.scss
new file mode 100644
index 0000000..d8c3eb3
--- /dev/null
+++ b/lab-nathan/frontend/src/components/app/_app.scss
@@ -0,0 +1,5 @@
+.cfgram {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/app/index.js b/lab-nathan/frontend/src/components/app/index.js
new file mode 100644
index 0000000..98833c4
--- /dev/null
+++ b/lab-nathan/frontend/src/components/app/index.js
@@ -0,0 +1,30 @@
+import './_app.scss';
+import React from 'react';
+import {BrowserRouter, Route} from 'react-router-dom';
+import Landing from '../landing';
+import Dashboard from '../dashboard';
+import Settings from '../settings';
+import Header from '../header';
+import Footer from '../footer';
+import Content from '../content';
+
+
+class App extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default App;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/avatar/_avatar.scss b/lab-nathan/frontend/src/components/avatar/_avatar.scss
new file mode 100644
index 0000000..ecc8449
--- /dev/null
+++ b/lab-nathan/frontend/src/components/avatar/_avatar.scss
@@ -0,0 +1,8 @@
+@import '../../styles/lib/vars';
+
+.avatar {
+ & img {
+ height: 48px;
+ margin-right: $gutter / 2;
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/avatar/index.js b/lab-nathan/frontend/src/components/avatar/index.js
new file mode 100644
index 0000000..008de3b
--- /dev/null
+++ b/lab-nathan/frontend/src/components/avatar/index.js
@@ -0,0 +1,8 @@
+import './_avatar.scss';
+import React from 'react'
+
+export default (props) => (
+
+

+
+)
diff --git a/lab-nathan/frontend/src/components/content/_content.scss b/lab-nathan/frontend/src/components/content/_content.scss
new file mode 100644
index 0000000..bd118e4
--- /dev/null
+++ b/lab-nathan/frontend/src/components/content/_content.scss
@@ -0,0 +1,5 @@
+main {
+ flex: 1;
+ text-align: center;
+ background: url('../../assets/noise.png');
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/content/index.js b/lab-nathan/frontend/src/components/content/index.js
new file mode 100644
index 0000000..7150ea2
--- /dev/null
+++ b/lab-nathan/frontend/src/components/content/index.js
@@ -0,0 +1,14 @@
+import './_content.scss';
+import React from 'react';
+
+class Content extends React.Component {
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+export default Content;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/dashboard/_dashboard.scss b/lab-nathan/frontend/src/components/dashboard/_dashboard.scss
new file mode 100644
index 0000000..c9db236
--- /dev/null
+++ b/lab-nathan/frontend/src/components/dashboard/_dashboard.scss
@@ -0,0 +1,5 @@
+.dashboard {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/dashboard/index.js b/lab-nathan/frontend/src/components/dashboard/index.js
new file mode 100644
index 0000000..8cf8c86
--- /dev/null
+++ b/lab-nathan/frontend/src/components/dashboard/index.js
@@ -0,0 +1,51 @@
+import './_dashboard.scss';
+import React from 'react'
+import {connect} from 'react-redux'
+import * as util from '../../lib/utilities.js'
+import * as photoActions from '../../actions/photo-actions.js'
+
+import PhotoForm from '../photo-form'
+import PhotoItem from '../photo-item'
+
+class DashboardContainer extends React.Component {
+ constructor(props){
+ super(props);
+ }
+
+ componentDidMount(){
+ this.props.photosFetch()
+ .catch(util.logError);
+ }
+
+ render(){
+ return (
+
+
{
+ return this.props.photoCreate(photo)
+ .catch(console.error)
+ }}
+ />
+ {this.props.photos.map(photo =>
+
+ )}
+
+ )
+ }
+}
+
+let mapStateToProps = (state) => ({
+ profile: state.profile,
+ photos: state.photos,
+})
+
+let mapDispatchToProps = (dispatch) => ({
+ photoCreate: (photo) => dispatch(photoActions.userPhotoCreateRequest(photo)),
+ photosFetch: (photos) => dispatch(photoActions.userPhotosFetchRequest()),
+})
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(DashboardContainer)
diff --git a/lab-nathan/frontend/src/components/footer/_footer.scss b/lab-nathan/frontend/src/components/footer/_footer.scss
new file mode 100644
index 0000000..5a7221a
--- /dev/null
+++ b/lab-nathan/frontend/src/components/footer/_footer.scss
@@ -0,0 +1,7 @@
+@import '../../styles/lib/vars';
+
+footer {
+ min-height: 50px;
+ background: $bookend-background;
+ box-shadow: $bookend-shadow;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/footer/index.js b/lab-nathan/frontend/src/components/footer/index.js
new file mode 100644
index 0000000..5bd592d
--- /dev/null
+++ b/lab-nathan/frontend/src/components/footer/index.js
@@ -0,0 +1,14 @@
+import './_footer.scss';
+import React from 'react';
+
+class Footer extends React.Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default Footer;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/google-oauth/_google-oauth.scss b/lab-nathan/frontend/src/components/google-oauth/_google-oauth.scss
new file mode 100644
index 0000000..cedbd62
--- /dev/null
+++ b/lab-nathan/frontend/src/components/google-oauth/_google-oauth.scss
@@ -0,0 +1,31 @@
+.google-oauth {
+ background-color: #4285F4;
+ color: white;
+ font-family: 'Roboto', sans-serif;
+ font-size: 14px;
+ margin: 0 auto;
+ width: 175px;
+ height: 40px;
+ border-radius: 3px;
+ display: block;
+ text-align: left;
+
+
+ & div {
+ display: inline-block;
+ vertical-align: middle;
+ height: 40px;
+ width: 40px;
+ }
+
+ & svg {
+ height: 40px;
+ width: 40px;
+ }
+
+ & p {
+ display: inline-block;
+ vertical-align: middle;
+ padding-left: 5px;
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/google-oauth/index.js b/lab-nathan/frontend/src/components/google-oauth/index.js
new file mode 100644
index 0000000..45f1a9b
--- /dev/null
+++ b/lab-nathan/frontend/src/components/google-oauth/index.js
@@ -0,0 +1,27 @@
+import './_google-oauth.scss';
+import React from 'react';
+import superagent from 'superagent';
+
+class GoogleOAuth extends React.Component {
+ render() {
+ let data = require(`../../assets/btn_google_dark_normal_ios.svg`);
+ let innerHtml = {__html: data};
+
+ let AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
+ let clientIDQuery = `client_id=${__GOOGLE_CLIENT_ID__}`
+ let responseTypeQuery = 'response_type=code';
+ let scopeQuery = 'scope=openid%20profile%20email';
+ let promptQuery = 'prompt=consent';
+ let redirectURIQuery = 'redirect_uri=http://localhost:3000/oauth/google/code';
+ let formattedURI = `${AUTH_URL}?${clientIDQuery}&${responseTypeQuery}&${scopeQuery}&${promptQuery}&${redirectURIQuery}`;
+
+ return (
+
+
+ Sign in with Google
+
+ );
+ }
+}
+
+export default GoogleOAuth;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/header/_header.scss b/lab-nathan/frontend/src/components/header/_header.scss
new file mode 100644
index 0000000..8d52b4f
--- /dev/null
+++ b/lab-nathan/frontend/src/components/header/_header.scss
@@ -0,0 +1,14 @@
+@import '../../styles/lib/vars';
+
+header {
+ min-height: 75px;
+ background: $bookend-background;
+ box-shadow: $bookend-shadow;
+
+ display: flex;
+ align-items: center;
+}
+
+#logout-button {
+ margin-right: $gutter / 2;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/header/index.js b/lab-nathan/frontend/src/components/header/index.js
new file mode 100644
index 0000000..738320d
--- /dev/null
+++ b/lab-nathan/frontend/src/components/header/index.js
@@ -0,0 +1,85 @@
+import './_header.scss';
+import React from 'react';
+import {Link} from 'react-router-dom';
+import Logo from '../logo';
+import Menu from '../menu';
+import MenuItem from '../menu-item';
+import Avatar from '../avatar';
+import * as util from '../../lib/utilities.js';
+import {tokenSet, logout} from '../../actions/token-actions.js';
+import {profileFetchRequest} from '../../actions/profile-actions.js';
+import {connect} from 'react-redux';
+
+class Header extends React.Component {
+ constructor(props){
+ super(props)
+ this.validateRoute = this.validateRoute.bind(this)
+ this.handleLogout = this.handleLogout.bind(this)
+ }
+
+ componentDidMount(){
+ this.validateRoute(this.props)
+ }
+
+ validateRoute(props){
+ let {match, history} = props
+ let token = util.readCookie('X-Sluggram-Token')
+
+ if(!token){
+ return history.replace('/welcome/signup')
+ }
+
+ this.props.tokenSet(token)
+ this.props.profileFetch()
+ .catch(() => {
+ console.log('PROFILE FETCH ERROR: user does not have a userProfile')
+ if(!match.url.startsWith('/settings')){
+ return history.replace('/settings')
+ }
+ })
+ }
+
+ handleLogout(){
+ this.props.logout()
+ this.props.history.push('/welcome/login')
+ }
+
+ render() {
+ return (
+
+
+ {util.renderIf(!this.props.loggedIn,
+
+ )}
+ {util.renderIf(this.props.loggedIn,
+
+ )}
+ {util.renderIf(this.props.profile,
+
+ )}
+ {util.renderIf(this.props.loggedIn,
+
+ )}
+
+ );
+ }
+}
+
+let mapStateToProps = (state) => ({
+ loggedIn: !!state.token,
+ profile: state.profile
+});
+
+let mapDispatchToProps = (dispatch) => ({
+ tokenSet: (token) => dispatch(tokenSet(token)),
+ logout: () => dispatch(logout()),
+ profileFetch: () => dispatch(profileFetchRequest())
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Header);
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/landing/_landing.scss b/lab-nathan/frontend/src/components/landing/_landing.scss
new file mode 100644
index 0000000..3dbe16b
--- /dev/null
+++ b/lab-nathan/frontend/src/components/landing/_landing.scss
@@ -0,0 +1,6 @@
+@import '../../styles/lib/vars';
+
+.landing {
+ height: 100%;
+ display: flex;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/landing/index.js b/lab-nathan/frontend/src/components/landing/index.js
new file mode 100644
index 0000000..cba8eb5
--- /dev/null
+++ b/lab-nathan/frontend/src/components/landing/index.js
@@ -0,0 +1,29 @@
+import './_landing.scss';
+import React from 'react';
+import {connect} from 'react-redux';
+import {signupRequest, loginRequest} from '../../actions/token-actions.js';
+import Login from '../login';
+
+class Landing extends React.Component {
+ render() {
+ let {params} = this.props.match;
+ let handleComplete = params.auth === 'login'
+ ? this.props.login
+ : this.props.signup;
+
+ return (
+
+
+
+ );
+ }
+}
+
+let mapDispatchToProps = (dispatch) =>({
+ signup: (user) => dispatch(signupRequest(user)),
+ login: (user) => dispatch(loginRequest(user))
+})
+
+export default connect(null, mapDispatchToProps)(Landing);
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/login/_login.scss b/lab-nathan/frontend/src/components/login/_login.scss
new file mode 100644
index 0000000..32d6b85
--- /dev/null
+++ b/lab-nathan/frontend/src/components/login/_login.scss
@@ -0,0 +1,34 @@
+@import '../../styles/lib/vars';
+
+.user-form {
+ background: url('../../assets/noise.png') #dadada;
+ padding: $gutter * 1.5 $gutter;
+ margin: auto;
+ border-radius: 5px;
+ box-shadow: 3px 3px 10px -4px rgba(0, 0, 0, 0.75);
+
+ & input {
+ display: block;
+ margin: $gutter / 3 auto;
+ padding: $input-padding;
+ font-size: $input-font-size;
+ border-radius: $input-border-radius;
+ border: $input-border;
+ }
+
+ & button {
+ margin-top: $gutter / 2;
+ padding: $input-padding * 3 / 4 $input-padding * 2;
+ font-size: 1rem;
+ border-radius: 3px;
+ border: none;
+ background: url('../../assets/noise.png') #666;
+ color: white;
+ }
+
+ & .separator {
+ height: 1px;
+ background-color: #bbb;
+ margin: $gutter 0;
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/login/index.js b/lab-nathan/frontend/src/components/login/index.js
new file mode 100644
index 0000000..d5a9cf2
--- /dev/null
+++ b/lab-nathan/frontend/src/components/login/index.js
@@ -0,0 +1,94 @@
+import './_login.scss';
+import React from 'react';
+import * as utilities from '../../lib/utilities.js';
+import GoogleOAuth from '../google-oauth';
+
+class Login extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ username: '',
+ email: '',
+ password: '',
+ usernameError: null,
+ passwordError: null,
+ emailError: null,
+ error: null
+ }
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ handleChange(e) {
+ let {name, value} = e.target;
+ this.setState({
+ [name]: value,
+ usernameError: name === 'username' && !value ? 'username required' : null,
+ emailError: name === 'email' && !value ? 'email required' : null,
+ passwordError: name === 'password' && !value ? 'password required' : null
+ });
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.onComplete(this.state)
+ .then(() => this.setState({ username: '', email: '', password: '' }))
+ .catch(error => {
+ console.error(error);
+ this.setState({error});
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default Login;
diff --git a/lab-nathan/frontend/src/components/logo/_logo.scss b/lab-nathan/frontend/src/components/logo/_logo.scss
new file mode 100644
index 0000000..030910f
--- /dev/null
+++ b/lab-nathan/frontend/src/components/logo/_logo.scss
@@ -0,0 +1,27 @@
+@import '../../styles/lib/vars';
+
+#logo {
+ margin: 0 $gutter;
+ flex: 1;
+}
+
+#logo-text {
+ font-size: $logo-size;
+ font-weight: bold;
+ font-family: 'Playfair Display', serif;
+ color: $primary-dark;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+#logo-image {
+ height: 48px;
+ width: 48px;
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: $gutter / 4;
+}
+
+#path3869, #path3853 {
+ fill: $primary-dark;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/logo/index.js b/lab-nathan/frontend/src/components/logo/index.js
new file mode 100644
index 0000000..ec5bbae
--- /dev/null
+++ b/lab-nathan/frontend/src/components/logo/index.js
@@ -0,0 +1,18 @@
+import './_logo.scss';
+import React from 'react';
+
+class Logo extends React.Component {
+ render() {
+ let data = require(`../../assets/camera-icon.svg`)
+ let innerHtml = {__html: data}
+
+ return (
+
+ );
+ }
+}
+
+export default Logo;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/menu-item/_menu-item.scss b/lab-nathan/frontend/src/components/menu-item/_menu-item.scss
new file mode 100644
index 0000000..2010bb6
--- /dev/null
+++ b/lab-nathan/frontend/src/components/menu-item/_menu-item.scss
@@ -0,0 +1,14 @@
+@import '../../styles/lib/vars';
+
+.menu-item {
+ display: inline;
+ margin-left: $gutter / 2;
+ font-size: 1.125rem;
+ color: $primary-dark;
+ font-family: $font-family;
+
+ & a {
+ color: inherit;
+ text-decoration: none;
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/menu-item/index.js b/lab-nathan/frontend/src/components/menu-item/index.js
new file mode 100644
index 0000000..c90460a
--- /dev/null
+++ b/lab-nathan/frontend/src/components/menu-item/index.js
@@ -0,0 +1,12 @@
+import './_menu-item.scss';
+import React from 'react';
+
+class MenuItem extends React.Component {
+ render() {
+ return (
+ {this.props.children}
+ );
+ }
+}
+
+export default MenuItem;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/menu/_menu.scss b/lab-nathan/frontend/src/components/menu/_menu.scss
new file mode 100644
index 0000000..51c2273
--- /dev/null
+++ b/lab-nathan/frontend/src/components/menu/_menu.scss
@@ -0,0 +1,6 @@
+@import '../../styles/lib/vars';
+
+.menu {
+ margin-right: $gutter;
+ font-family: $font-family;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/menu/index.js b/lab-nathan/frontend/src/components/menu/index.js
new file mode 100644
index 0000000..d13ed2b
--- /dev/null
+++ b/lab-nathan/frontend/src/components/menu/index.js
@@ -0,0 +1,14 @@
+import './_menu.scss';
+import React from 'react';
+
+class Menu extends React.Component {
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+export default Menu;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/photo-form/_photo-form.scss b/lab-nathan/frontend/src/components/photo-form/_photo-form.scss
new file mode 100644
index 0000000..8015689
--- /dev/null
+++ b/lab-nathan/frontend/src/components/photo-form/_photo-form.scss
@@ -0,0 +1,41 @@
+@import '../../styles/lib/vars';
+
+.photo-form {
+ background: url('../../assets/noise.png') #dadada;
+ padding: $gutter;
+ margin: auto;
+ border-radius: 5px;
+ box-shadow: 3px 3px 10px -4px rgba(0, 0, 0, 0.75);
+
+ & img {
+ max-height: 300px;
+ }
+
+ & input {
+ display: block;
+ margin: $gutter / 3 auto;
+ padding: $input-padding;
+ font-size: $input-font-size;
+ border-radius: $input-border-radius;
+ }
+
+ & button {
+ margin-top: $gutter / 2;
+ padding: $input-padding * 3 / 4 $input-padding * 2;
+ font-size: 1rem;
+ border-radius: 3px;
+ border: none;
+ background: url('../../assets/noise.png') #666;
+ color: white;
+ }
+
+ & textarea {
+ display: block;
+ width: 100%;
+ min-height: 100px;
+ font-size: inherit;
+ font-family: helvetica;
+ padding: 10px;
+ box-sizing: border-box;
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/photo-form/index.js b/lab-nathan/frontend/src/components/photo-form/index.js
new file mode 100644
index 0000000..e69537a
--- /dev/null
+++ b/lab-nathan/frontend/src/components/photo-form/index.js
@@ -0,0 +1,70 @@
+import './_photo-form.scss';
+import React from 'react';
+import * as util from '../../lib/utilities.js'
+
+class PhotoForm extends React.Component {
+ constructor(props){
+ super(props)
+ this.state = props.photo
+ ? props.photo
+ : {description: '' , preview: '', photo: null}
+
+ this.handleChange = this.handleChange.bind(this)
+ this.handleSubmit = this.handleSubmit.bind(this)
+ }
+
+ handleChange(e){
+ let {name} = e.target
+ if(name == 'description'){
+ this.setState({description: e.target.value})
+ }
+
+ if(name == 'photo'){
+ let {files} = e.target
+ let photo = files[0]
+ this.setState({photo})
+ util.photoToDataURL(photo)
+ .then(preview => this.setState({preview}))
+ .catch(console.error)
+ }
+ }
+
+ handleSubmit(e){
+ e.preventDefault()
+ return this.props.onComplete(this.state)
+ .then(() => {
+ if(!this.props.profile){
+ this.setState({description: '' , preview: '', photo: null})
+ }
+ })
+ }
+
+ render(){
+ return (
+
+ )
+ }
+}
+
+export default PhotoForm
diff --git a/lab-nathan/frontend/src/components/photo-item/index.js b/lab-nathan/frontend/src/components/photo-item/index.js
new file mode 100644
index 0000000..2a94656
--- /dev/null
+++ b/lab-nathan/frontend/src/components/photo-item/index.js
@@ -0,0 +1,72 @@
+import React from 'react'
+import {connect} from 'react-redux'
+
+import PhotoForm from '../photo-form'
+import * as util from '../../lib/utilities.js'
+import * as photoActions from '../../actions/photo-actions.js'
+
+export class PhotoItem extends React.Component {
+ constructor(props){
+ super(props)
+
+ this.state = {
+ editing: false
+ }
+
+ this.handleDelete = this.handleDelete.bind(this)
+ this.handleUpdate = this.handleUpdate.bind(this)
+ }
+
+ handleDelete(){
+ return this.props.deletePhoto(this.props.photo)
+ .then(console.log)
+ .catch(console.error)
+ }
+
+ handleUpdate(photo){
+ return this.props.updatePhoto(photo)
+ .then(() => {
+ this.setState({editing: false})
+ })
+ .catch(console.error)
+ }
+
+
+ render(){
+ let {photo} = this.props
+ return (
+
+ {util.renderIf(!this.state.editing,
+
+

+
{photo.description}
+
+
this.setState({editing: true})} className='fa fa-pencil fa-3x' />
+
+ )}
+
+ {util.renderIf(this.state.editing,
+
+ )}
+
+ )
+ }
+}
+
+let mapStateToProps = () => ({})
+let mapDispatchToProps = (dispatch) => ({
+ deletePhoto: (photo) => dispatch(photoActions.userPhotoDeleteRequest(photo)),
+ updatePhoto: (photo) => dispatch(photoActions.userPhotoUpdateRequest(photo)),
+})
+
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(PhotoItem)
diff --git a/lab-nathan/frontend/src/components/profile-form/_profile-form.scss b/lab-nathan/frontend/src/components/profile-form/_profile-form.scss
new file mode 100644
index 0000000..3ba7c44
--- /dev/null
+++ b/lab-nathan/frontend/src/components/profile-form/_profile-form.scss
@@ -0,0 +1,41 @@
+@import '../../styles/lib/vars';
+
+.profile-form {
+ background: url('../../assets/noise.png') #dadada;
+ padding: $gutter;
+ margin: auto;
+ border-radius: 5px;
+ box-shadow: 3px 3px 10px -4px rgba(0, 0, 0, 0.75);
+
+ & img {
+ max-height: 300px;
+ }
+
+ & input {
+ display: block;
+ margin: $gutter / 3 auto;
+ padding: $input-padding;
+ font-size: $input-font-size;
+ border-radius: $input-border-radius;
+ }
+
+ & button {
+ margin-top: $gutter / 2;
+ padding: $input-padding * 3 / 4 $input-padding * 2;
+ font-size: 1rem;
+ border-radius: 3px;
+ border: none;
+ background: url('../../assets/noise.png') #666;
+ color: white;
+ }
+
+ & textarea {
+ display: block;
+ width: 100%;
+ min-height: 100px;
+ font-size: inherit;
+ font-family: helvetica;
+ padding: 10px;
+ box-sizing: border-box;
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/profile-form/index.js b/lab-nathan/frontend/src/components/profile-form/index.js
new file mode 100644
index 0000000..00fc820
--- /dev/null
+++ b/lab-nathan/frontend/src/components/profile-form/index.js
@@ -0,0 +1,72 @@
+import './_profile-form.scss';
+import React from 'react';
+import * as util from '../../lib/utilities.js';
+
+class ProfileForm extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = props.profile
+ ? { ...props.profile, preview: '' }
+ : { bio: '', avatar: null, preview: '' }
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ componentWillReceiveProps(props) {
+ if (props.profile) {
+ this.setState(props.profile);
+ }
+ }
+
+ handleChange(e) {
+ let {type, name} = e.target;
+ if (name === 'bio') {
+ this.setState({ bio: e.target.value });
+ }
+
+ if (name === 'avatar') {
+ let {files} = e.target;
+ let avatar = files[0];
+
+ this.setState({ avatar });
+
+ util.photoToDataURL(avatar)
+ .then(preview => this.setState({preview}))
+ .catch(console.error);
+ }
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ this.props.onComplete(this.state);
+ }
+
+ render() {
+ console.log(this.state.preview);
+ return (
+
+ )
+ }
+}
+
+export default ProfileForm;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/profile/index.js b/lab-nathan/frontend/src/components/profile/index.js
new file mode 100644
index 0000000..cc93ed6
--- /dev/null
+++ b/lab-nathan/frontend/src/components/profile/index.js
@@ -0,0 +1,7 @@
+import React from 'react'
+
+export default (props) => (
+
+

+
+)
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/settings/_settings.scss b/lab-nathan/frontend/src/components/settings/_settings.scss
new file mode 100644
index 0000000..07e3b58
--- /dev/null
+++ b/lab-nathan/frontend/src/components/settings/_settings.scss
@@ -0,0 +1,7 @@
+@import '../../styles/lib/vars';
+
+.settings-container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/components/settings/index.js b/lab-nathan/frontend/src/components/settings/index.js
new file mode 100644
index 0000000..e07c401
--- /dev/null
+++ b/lab-nathan/frontend/src/components/settings/index.js
@@ -0,0 +1,56 @@
+import './_settings.scss';
+import React from 'react';
+import {connect} from 'react-redux';
+import ProfileForm from '../profile-form';
+import {profileCreateRequest, profileUpdateRequest} from '../../actions/profile-actions.js';
+
+class SettingsContainer extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleProfileCreate = this.handleProfileCreate.bind(this);
+ this.handleProfileUpdate = this.handleProfileUpdate.bind(this);
+ }
+
+ handleProfileCreate(profile) {
+ return this.props.profileCreate(profile)
+ .then(response => {
+ console.log(response);
+ })
+ .catch(console.error);
+ }
+
+ handleProfileUpdate(profile) {
+ return this.props.profileUpdate(profile)
+ .catch(console.error)
+ }
+
+ render() {
+ let handleComplete = this.props.profile
+ ? this.handleProfileUpdate
+ : this.handleProfileCreate;
+
+ let buttonText = this.props.profile
+ ? 'update profile'
+ : 'create profile';
+
+ return (
+
+ );
+ }
+}
+
+let mapStateToProps = (state) => ({
+ profile: state.profile
+})
+
+let mapDispatchToProps = (dispatch) => ({
+ profileCreate: (profile) => dispatch(profileCreateRequest(profile)),
+ profileUpdate: (profile) => dispatch(profileUpdateRequest(profile))
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(SettingsContainer);
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/index.html b/lab-nathan/frontend/src/index.html
new file mode 100644
index 0000000..44d42a5
--- /dev/null
+++ b/lab-nathan/frontend/src/index.html
@@ -0,0 +1,9 @@
+
+
+
+ CF Gram
+
+
+
+
+
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/lib/create-store.js b/lab-nathan/frontend/src/lib/create-store.js
new file mode 100644
index 0000000..b52ae88
--- /dev/null
+++ b/lab-nathan/frontend/src/lib/create-store.js
@@ -0,0 +1,6 @@
+import {createStore, applyMiddleware} from 'redux';
+import reducers from '../reducers';
+import thunk from './thunk.js';
+import reporter from './reporter.js';
+
+export default () => createStore(reducers, applyMiddleware(thunk, reporter));
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/lib/reporter.js b/lab-nathan/frontend/src/lib/reporter.js
new file mode 100644
index 0000000..7350185
--- /dev/null
+++ b/lab-nathan/frontend/src/lib/reporter.js
@@ -0,0 +1,14 @@
+export default store => next => action => {
+ console.log('__ACTION__', action);
+
+ try {
+ let result = next(action);
+ console.log('__STATE__', store.getState());
+ return result;
+ }
+ catch(error) {
+ error.action = action;
+ console.error('__ERROR__', error);
+ return error;
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/lib/thunk.js b/lab-nathan/frontend/src/lib/thunk.js
new file mode 100644
index 0000000..c3a44ca
--- /dev/null
+++ b/lab-nathan/frontend/src/lib/thunk.js
@@ -0,0 +1,6 @@
+export default store => next => action => {
+ if (typeof action === 'function')
+ return action(store.dispatch, store.getState);
+ else
+ return next(action);
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/lib/utilities.js b/lab-nathan/frontend/src/lib/utilities.js
new file mode 100644
index 0000000..025acc9
--- /dev/null
+++ b/lab-nathan/frontend/src/lib/utilities.js
@@ -0,0 +1,73 @@
+export const log = (...args) =>
+__DEBUG__ ? console.log(...args) : undefined;
+
+export const logError = (...args) =>
+__DEBUG__ ? console.error(...args) : undefined;
+
+export const renderIf = (test, component) => test ? component : undefined;
+
+export const classToggler = (options) =>
+Object.keys(options).filter(key => Boolean(options)).join(' ');
+
+export const map = (list, ...args) =>
+Array.prototype.map.apply(list, args);
+
+export const filter = (list, ...args) =>
+Array.prototype.filter.apply(list, args);
+
+export const reduce = (list, ...args) =>
+Array.prototype.reduce.apply(list, args);
+
+export const photoToDataURL = (file) => {
+ return new Promise((resolve, reject) => {
+ let reader = new FileReader();
+
+ reader.addEventListener('load', () => {
+ resolve(reader.result);
+ });
+
+ reader.addEventListener('error', () => {
+ reject(reader.error);
+ });
+
+ if (file) {
+ return reader.readAsDataURL(file);
+ }
+
+ return reject(new Error('USAGE ERROR: requires file'));
+ });
+}
+
+// https://stackoverflow.com/questions/14573223/set-cookie-and-get-cookie-with-javascript
+export const readCookie = (name) => {
+ var nameEquals = name + '=';
+ var attributes = document.cookie.split(';');
+
+ for (var i = 0; i < attributes.length; i++) {
+ var attribute = attributes[i];
+
+ while (attribute.charAt(0) == ' ') {
+ attribute = attribute.substring(1, attribute.length);
+ }
+
+ if (attribute.indexOf(nameEquals) == 0) {
+ return attribute.substring(nameEquals.length, attribute.length);
+ }
+ }
+
+ return null;
+}
+
+export const createCookie = (name,value,days) => {
+ if (days) {
+ var date = new Date();
+ date.setTime(date.getTime()+(days*24*60*60*1000));
+ var expires = "; expires="+date.toGMTString();
+ }
+ else var expires = "";
+ document.cookie = name+"="+value+expires+"; path=/";
+}
+
+export const deleteCookie = (name) => {
+ createCookie(name,"",-1);
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/main.js b/lab-nathan/frontend/src/main.js
new file mode 100644
index 0000000..811dacf
--- /dev/null
+++ b/lab-nathan/frontend/src/main.js
@@ -0,0 +1,16 @@
+import './styles/main.scss';
+import React from 'react';
+import ReactDom from 'react-dom';
+import {Provider} from 'react-redux';
+import App from './components/app';
+import createStore from './lib/create-store.js';
+
+let AppContainer = () => {
+ return (
+
+
+
+ )
+}
+
+ReactDom.render(, document.getElementById('root'));
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/reducers/index.js b/lab-nathan/frontend/src/reducers/index.js
new file mode 100644
index 0000000..6d09997
--- /dev/null
+++ b/lab-nathan/frontend/src/reducers/index.js
@@ -0,0 +1,10 @@
+import {combineReducers} from 'redux';
+import tokenReducer from './token-reducer.js';
+import profileReducer from './profile-reducer.js';
+import photosReducer from './photos-reducer.js';
+
+export default combineReducers({
+ token: tokenReducer,
+ profile: profileReducer,
+ photos: photosReducer
+});
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/reducers/photos-reducer.js b/lab-nathan/frontend/src/reducers/photos-reducer.js
new file mode 100644
index 0000000..8872dd5
--- /dev/null
+++ b/lab-nathan/frontend/src/reducers/photos-reducer.js
@@ -0,0 +1,17 @@
+export default (state=[], action) => {
+ let {type, payload} = action
+ switch(type){
+ case 'USER_PHOTOS_SET':
+ return payload
+ case 'USER_PHOTO_CREATE':
+ return [payload, ...state]
+ case 'USER_PHOTO_UPDATE':
+ return state.map(item => item._id === payload._id ? payload : item)
+ case 'USER_PHOTO_DELETE':
+ return state.filter(item => item._id !== payload._id)
+ case 'LOGOUT':
+ return []
+ default:
+ return state
+ }
+}
diff --git a/lab-nathan/frontend/src/reducers/profile-reducer.js b/lab-nathan/frontend/src/reducers/profile-reducer.js
new file mode 100644
index 0000000..96cea91
--- /dev/null
+++ b/lab-nathan/frontend/src/reducers/profile-reducer.js
@@ -0,0 +1,24 @@
+let validateProfileCreate = (profile) => {
+ if(!profile.avatar || !profile.bio || !profile._id
+ || !profile.owner || !profile.username || !profile.email){
+ throw new Error('VALIDATION ERROR: profile requires avatar and bio')
+ }
+}
+
+export default (state=null, action) => {
+ let {type, payload} = action
+ switch(type){
+ case 'PROFILE_CREATE':
+ validateProfileCreate(payload)
+ return payload
+ case 'PROFILE_UPDATE':
+ if(!state)
+ throw new Error('USAGE ERROR: can not update when profile is null')
+ validateProfileCreate(payload)
+ return {...state, ...payload}
+ case 'LOGOUT':
+ return null
+ default:
+ return state
+ }
+}
diff --git a/lab-nathan/frontend/src/reducers/token-reducer.js b/lab-nathan/frontend/src/reducers/token-reducer.js
new file mode 100644
index 0000000..7e41a61
--- /dev/null
+++ b/lab-nathan/frontend/src/reducers/token-reducer.js
@@ -0,0 +1,12 @@
+export default (state = null, action) => {
+ let {type, payload} = action;
+ switch(type) {
+ case 'TOKEN_SET':
+ return payload
+ case 'TOKEN_DELETE':
+ case 'LOGOUT':
+ return null
+ default:
+ return state
+ }
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/styles/base/_base.scss b/lab-nathan/frontend/src/styles/base/_base.scss
new file mode 100644
index 0000000..47a749b
--- /dev/null
+++ b/lab-nathan/frontend/src/styles/base/_base.scss
@@ -0,0 +1,3 @@
+html, body, #root {
+ height: 100%;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/styles/base/_reset.scss b/lab-nathan/frontend/src/styles/base/_reset.scss
new file mode 100644
index 0000000..af94440
--- /dev/null
+++ b/lab-nathan/frontend/src/styles/base/_reset.scss
@@ -0,0 +1,48 @@
+/* http://meyerweb.com/eric/tools/css/reset/
+ v2.0 | 20110126
+ License: none (public domain)
+*/
+
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-size: 100%;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article, aside, details, figcaption, figure,
+footer, header, hgroup, menu, nav, section {
+ display: block;
+}
+body {
+ line-height: 1;
+}
+ol, ul {
+ list-style: none;
+}
+blockquote, q {
+ quotes: none;
+}
+blockquote:before, blockquote:after,
+q:before, q:after {
+ content: '';
+ content: none;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/styles/layout/_content.scss b/lab-nathan/frontend/src/styles/layout/_content.scss
new file mode 100644
index 0000000..e69de29
diff --git a/lab-nathan/frontend/src/styles/layout/_footer.scss b/lab-nathan/frontend/src/styles/layout/_footer.scss
new file mode 100644
index 0000000..e69de29
diff --git a/lab-nathan/frontend/src/styles/layout/_header.scss b/lab-nathan/frontend/src/styles/layout/_header.scss
new file mode 100644
index 0000000..e69de29
diff --git a/lab-nathan/frontend/src/styles/lib/_vars.scss b/lab-nathan/frontend/src/styles/lib/_vars.scss
new file mode 100644
index 0000000..2fe7b91
--- /dev/null
+++ b/lab-nathan/frontend/src/styles/lib/_vars.scss
@@ -0,0 +1,11 @@
+$primary: hsla(140, 52%, 55%, 1);
+$primary-dark: hsla(140, 52%, 25%, 1);;
+$bookend-background: url('../../assets/noise.png') hsl(140, 52%, 55%);
+$bookend-shadow: 0px 0px 5px 2px rgba(0,0,0,0.5);
+$logo-size: 2.25rem;
+$gutter: 30px;
+$font-family: 'helvetica', sans-serif;
+$input-padding: 10px;
+$input-font-size: 1.125rem;
+$input-border-radius: 3px;
+$input-border: 1px solid #ddd;
\ No newline at end of file
diff --git a/lab-nathan/frontend/src/styles/main.scss b/lab-nathan/frontend/src/styles/main.scss
new file mode 100644
index 0000000..bcea48b
--- /dev/null
+++ b/lab-nathan/frontend/src/styles/main.scss
@@ -0,0 +1,5 @@
+@import url('https://fonts.googleapis.com/css?family=Playfair+Display');
+@import url('https://fonts.googleapis.com/css?family=Roboto');
+@import "./lib/vars";
+@import "./base/reset";
+@import "./base/base";
\ No newline at end of file
diff --git a/lab-nathan/frontend/webpack.config.js b/lab-nathan/frontend/webpack.config.js
new file mode 100644
index 0000000..78950d4
--- /dev/null
+++ b/lab-nathan/frontend/webpack.config.js
@@ -0,0 +1,97 @@
+'use strict';
+
+require('dotenv').config();
+
+const production = process.env.NODE_ENV === 'production';
+
+const { DefinePlugin, EnvironmentPlugin } = require('webpack');
+const HtmlPlugin = require('html-webpack-plugin');
+const CleanPlugin = require('clean-webpack-plugin');
+const UglifyPlugin = require('uglifyjs-webpack-plugin');
+const ExtractPlugin = require('extract-text-webpack-plugin');
+
+console.log(process.env.API_URL);
+
+let plugins = [
+ new EnvironmentPlugin(['NODE_ENV']),
+ new ExtractPlugin('bundle-[hash].css'),
+ new HtmlPlugin({ template: `${__dirname}/src/index.html` }),
+ new DefinePlugin({
+ '__DEBUG__': JSON.stringify('development'),
+ '__API_URL__': JSON.stringify(process.env.API_URL),
+ '__GOOGLE_CLIENT_ID__': JSON.stringify(process.env.GOOGLE_CLIENT_ID)
+ })
+];
+
+if (production) {
+ plugins = plugins.concat([ new CleanPlugin(), new UglifyPlugin() ]);
+}
+
+module.exports = {
+ plugins,
+ entry: `${__dirname}/src/main.js`,
+ devServer: {
+ historyApiFallback: true
+ },
+ devtool: production ? undefined : 'eval',
+ output: {
+ path: `${__dirname}/build`,
+ filename: 'bundle-[hash].js',
+ publicPath: process.env.CDN_URL
+ },
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: 'babel-loader'
+ },
+ {
+ test: /\.scss$/,
+ loader: ExtractPlugin.extract(['css-loader', 'sass-loader'])
+ },
+ {
+ test: /\.(svg)$/,
+ loader: 'raw-loader',
+ },
+ {
+ test: /\.(woff|woff2|ttf|eot|glyph|\.svg)$/,
+ exclude: /\camera-icon.svg|btn_google_dark_normal_ios.svg$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 10000,
+ name: 'font/[name].[ext]'
+ }
+ }
+ ]
+ },
+ {
+ test: /\.(jpg|jpeg|gif|png|tiff|svg)$/,
+ exclude: /\glyph.svg|camera-icon.svg|btn_google_dark_normal_ios.svg$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 6000,
+ name: 'image/[name].[ext]'
+ },
+ }
+ ]
+ },
+ {
+ test: /\.(mp3|aac|aiff|wav|flac|m4a|mp4|ogg|ape)$/,
+ exclude: /\glyph.svg|camera-icon.svg|btn_google_dark_normal_ios.svg$/,
+ use: [
+ {
+ loader: 'file-loader',
+ options: {
+ name: 'audio/[name].[ext]'
+ }
+ }
+ ]
+ }
+ ]
+ }
+};