diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..345130c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,136 @@
+# Created by https://www.gitignore.io/api/osx,vim,node,macos,windows
+
+### macOS ###
+*.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Typescript v1 declaration files
+typings/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+
+
+### OSX ###
+
+# Icon must end with two \r
+
+# Thumbnails
+
+# Files that might appear in the root of a volume
+
+# Directories potentially created on remote AFP share
+
+### Vim ###
+# swap
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-v][a-z]
+[._]sw[a-p]
+# session
+Session.vim
+# temporary
+.netrwhist
+*~
+# auto-generated tag files
+tags
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Folder config file
+Desktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.gitignore.io/api/osx,vim,node,macos,windows
diff --git a/frontend/.babelrc b/frontend/.babelrc
new file mode 100644
index 0000000..47c9ace
--- /dev/null
+++ b/frontend/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": ["es2015", "react"],
+ "plugins": ["transform-object-rest-spread"]
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..dc51e76
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "frontend",
+ "version": "1.0.0",
+ "description": "",
+ "main": "webpack.config.js",
+ "scripts": {
+ "build": "webpack",
+ "watch": "webpack-dev-server --inline --hot"
+ },
+ "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",
+ "node-sass": "^4.5.3",
+ "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",
+ "sass-loader": "^6.0.6",
+ "superagent": "^3.6.0",
+ "uglifyjs-webpack-plugin": "^0.4.6",
+ "url-loader": "^0.5.9",
+ "webpack": "^3.5.6",
+ "webpack-dev-server": "^2.7.1"
+ }
+}
diff --git a/frontend/src/action/auth-actions.js b/frontend/src/action/auth-actions.js
new file mode 100644
index 0000000..4cb0e90
--- /dev/null
+++ b/frontend/src/action/auth-actions.js
@@ -0,0 +1,35 @@
+import superagent from 'superagent';
+
+export const tokenSet = (token) => ({
+ type: 'TOKEN_SET',
+ payload: token
+})
+
+export const tokenDelete = () => ({
+ type: 'TOKEN_DELETE'
+})
+
+export const signupRequest = (user) => (dispatch) => {
+ return superagent.post(`${__API_URL__}/signup`)
+ .withCredentials()
+ .send(user)
+ .then(res => {
+ dispatch(tokenSet(res.text));
+ try {
+ localStorage.token = res.text;
+ } catch (err) {
+ console.log(err);
+ }
+ return res;
+ })
+}
+
+export const loginRequest = (user) => (dispatch) => {
+ return superagent.get(`${__API_URL__}/login`)
+ .withCredentials()
+ .auth(user.username, user.password)
+ .then(res => {
+ dispatch(tokenSet(res.text));
+ return res;
+ })
+}
diff --git a/frontend/src/action/photo-actions.js b/frontend/src/action/photo-actions.js
new file mode 100644
index 0000000..7234299
--- /dev/null
+++ b/frontend/src/action/photo-actions.js
@@ -0,0 +1,65 @@
+import superagent from 'superagent';
+
+export const userPhotoSet = (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,
+})
+
+export const userPhotosFetchRequest = (photo) => (dispatch, getState) => {
+ let {auth} = getState()
+ return superagent.get(`${__API_URL__}/photos/me`)
+ .set('Authorization', `Bearer ${auth}`)
+ .then(res => {
+ dispatch(userPhotosSet(res.body.data))
+ return res
+ })
+}
+
+export const userPhotoCreateRequest = (photo) => (dispatch, getState) => {
+ let {auth} = getState()
+ return superagent.post(`${__API_URL__}/photos`)
+ .set('Authorization', `Bearer ${auth}`)
+ .field('description', photo.description)
+ .attach('photo', photo.photo)
+ .then((res) => {
+ dispatch(userPhotoCreate(res.body))
+ return res
+ })
+}
+
+export const userPhotoDeleteRequest = (photo) => (dispatch, getState) => {
+ let {auth} = getState()
+ return superagent.delete(`${__API_URL__}/photos/${photo._id}`)
+ .set('Authorization', `Bearer ${auth}`)
+ .then(res => {
+ dispatch(userPhotoDelete(photo))
+ return res
+ })
+}
+
+export const userPhotoUpdateRequest = (photo) => (dispatch, getState) => {
+ let {auth} = getState()
+ return superagent.put(`${__API_URL__}/photos/${photo._id}`)
+ .set('Authorization', `Bearer ${auth}`)
+ .field('description', photo.description)
+ .attach('photo', photo.photo)
+ .then(res => {
+ dispatch(userPhotoUpdate(res.body))
+ return res
+ })
+}
diff --git a/frontend/src/action/profile-actions.js b/frontend/src/action/profile-actions.js
new file mode 100644
index 0000000..24256bd
--- /dev/null
+++ b/frontend/src/action/profile-actions.js
@@ -0,0 +1,23 @@
+import superagent from 'superagent';
+
+export const profileCreate = (profile) => ({
+ type: 'PROFILE_CREATE',
+ payload: profile
+})
+
+export const profileUpdate = (profile) => ({
+ type: 'PROFILE_UPDATE',
+ payload: profile
+})
+
+export const profileCreateRequest = (profile) => (dispatch, getState) => {
+ let {auth} = getState();
+ return superagent.post(`${__API_URL__}/profiles`)
+ .set('Authorization', `Bearer ${auth}`)
+ .field('bio', profile.bio)
+ .attach('avatar', profile.avatar)
+ .then( res => {
+ dispatch(profileCreate(res.body));
+ return res;
+ })
+}
diff --git a/frontend/src/component/app/index.js b/frontend/src/component/app/index.js
new file mode 100644
index 0000000..6935fe7
--- /dev/null
+++ b/frontend/src/component/app/index.js
@@ -0,0 +1,57 @@
+import React from 'react';
+import {connect} from 'react-redux';
+import {BrowserRouter, Route, Link} from 'react-router-dom';
+import LandingContainer from '../landing-container';
+import DashboardContainer from '../dashboard-container';
+import SettingsContainer from '../settings-container';
+import * as util from '../../lib/util.js';
+import {tokenSet} from '../../action/auth-actions.js';
+import {userProfileFetchRequest} from '../../action/profile-actions.js';
+
+class App extends React.Component {
+ constructor(props){
+ super(props);
+ }
+ componentDidMount() {
+ let token = util.readCookie('X-Sluggram-Token');
+ if (token) {
+ this.props.tokenSet(token);
+ }
+ console.log('DASH', this.state);
+ }
+
+ render() {
+ return (
+
+
+
+
+ cfgram
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+let mapStateToProps = (state) => ({
+ profile: state.profile
+})
+
+let mapDispatchToProps = (dispatch) => ({
+ tokenSet: (token) => dispatch(tokenSet(token))
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(App);
diff --git a/frontend/src/component/auth-form/index.js b/frontend/src/component/auth-form/index.js
new file mode 100644
index 0000000..e2cb130
--- /dev/null
+++ b/frontend/src/component/auth-form/index.js
@@ -0,0 +1,94 @@
+import React from 'react';
+import * as util from '../../lib/util.js';
+
+class AuthForm 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 AuthForm;
diff --git a/frontend/src/component/avatar/index.js b/frontend/src/component/avatar/index.js
new file mode 100644
index 0000000..8b6f06f
--- /dev/null
+++ b/frontend/src/component/avatar/index.js
@@ -0,0 +1,7 @@
+import React from 'react';
+
+export default (props) => (
+
+

+
+)
diff --git a/frontend/src/component/dashboard-container/_dashboard-container.scss b/frontend/src/component/dashboard-container/_dashboard-container.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/component/dashboard-container/index.js b/frontend/src/component/dashboard-container/index.js
new file mode 100644
index 0000000..308be82
--- /dev/null
+++ b/frontend/src/component/dashboard-container/index.js
@@ -0,0 +1,50 @@
+import './_dashboard-container.scss';
+import React from 'react';
+import {connect} from 'react-redux';
+import * as util from '../../lib/util.js';
+import * as photoActions from '../../action/photo-actions.js';
+
+import PhotoForm from '../photo-form';
+import PhotoItem from '../photo-item';
+
+class DashboardContainer extends React.Component {
+ constructor(props) {
+ super(props)
+ console.log(this.state);
+ }
+
+ componentDidMount() {
+ this.props.userPhotosFetch()
+ .catch(util.logError)
+ }
+
+ render() {
+ return (
+
+
Dashboard
+
{
+ return this.props.userPhotoCreate(photo)
+ .catch(console.error)
+ }}
+ />
+ {this.props.userPhotos.map(photo =>
+
+ )}
+
+ )
+ }
+}
+
+let mapStateToProps = (state) => ({
+ userProfile: state.userProfile,
+ userPhotos: state.userPhotos,
+})
+
+let mapDispatchToProps = (dispatch) => ({
+ userPhotoCreate: (photo) => dispatch(photoActions.userPhotoCreateRequest(photo)),
+ userPhotosFetch: (photos) => dispatch(photoActions.userPhotosFetchRequest()),
+})
+
+export default connect(mapStateToProps, mapDispatchToProps,)(DashboardContainer)
diff --git a/frontend/src/component/landing-container/index.js b/frontend/src/component/landing-container/index.js
new file mode 100644
index 0000000..fc47366
--- /dev/null
+++ b/frontend/src/component/landing-container/index.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import {connect} from 'react-redux';
+import AuthForm from '../auth-form';
+import * as util from '../../lib/util.js';
+import {signupRequest, loginRequest} from '../../action/auth-actions.js';
+
+class LandingContainer extends React.Component {
+ render() {
+ let {params} = this.props.match;
+
+ let handleComplete = params.auth === 'login'
+ ? this.props.login
+ : this.props.signup
+
+ return (
+
+ )
+ }
+}
+
+let mapDispatchToProps = (dispatch) => {
+ return {
+ signup: (user) => dispatch(signupRequest(user)),
+ login: (user) => dispatch(loginRequest(user))
+ }
+}
+
+export default connect(undefined, mapDispatchToProps)(LandingContainer);
diff --git a/frontend/src/component/photo-form/index.js b/frontend/src/component/photo-form/index.js
new file mode 100644
index 0000000..b883569
--- /dev/null
+++ b/frontend/src/component/photo-form/index.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import * as util from '../../lib/util.js';
+
+class PhotoForm extends React.Component {
+ constructor(props) {
+ super(props);
+ console.log(this.state);
+ this.state = props.photo
+ ? props.photo
+ : {description: '', preview: '', photo: null}
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleSubmit = this.handleSubmit.bind(this);
+ }
+
+ handleChange(e) {
+ console.log('event', e.target);
+ 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/frontend/src/component/photo-item/index.js b/frontend/src/component/photo-item/index.js
new file mode 100644
index 0000000..f1d15ff
--- /dev/null
+++ b/frontend/src/component/photo-item/index.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import {connect} from 'react-redux';
+import PhotoForm from '../photo-form';
+import * as util from '../../lib/util.js';
+import * as photoActions from '../../action/photo-actions.js';
+
+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() {
+ 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}
+
+
+
+ )}
+
+ {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/frontend/src/component/profile-form/index.js b/frontend/src/component/profile-form/index.js
new file mode 100644
index 0000000..51caac0
--- /dev/null
+++ b/frontend/src/component/profile-form/index.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import * as util from '../../lib/util.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() {
+ return (
+
+ )
+ }
+}
+
+export default ProfileForm;
diff --git a/frontend/src/component/settings-container/index.js b/frontend/src/component/settings-container/index.js
new file mode 100644
index 0000000..a82550f
--- /dev/null
+++ b/frontend/src/component/settings-container/index.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import {connect} from 'react-redux';
+import ProfileForm from '../profile-form';
+import {profileCreateRequest} from '../../action/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(res => {
+ console.log('profile create response:', res);
+ })
+ .catch(console.error)
+ }
+
+
+ handleProfileUpdate(profile){
+ return this.props.ProfileUpdate(profile)
+ .catch(console.error)
+ }
+
+ render() {
+ let handleComplete = this.props.profile
+ ? this.handleProfileCreate
+ : this.handleProfileUpdate
+
+ return (
+
+
Tell us about yourself
+
+
+ )
+ }
+}
+
+let mapStateToProps = (state) => ({
+ profile: state.profile
+})
+
+let mapDispatchToProps = (dispatch) => ({
+ profileCreate: (profile) => dispatch(profileCreateRequest(profile))
+})
+
+export default connect(mapStateToProps, mapDispatchToProps)(SettingsContainer);
diff --git a/frontend/src/index.html b/frontend/src/index.html
new file mode 100644
index 0000000..968a343
--- /dev/null
+++ b/frontend/src/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Auth Lab
+
+
+
+
+
diff --git a/frontend/src/lib/app-create-store.js b/frontend/src/lib/app-create-store.js
new file mode 100644
index 0000000..432c33d
--- /dev/null
+++ b/frontend/src/lib/app-create-store.js
@@ -0,0 +1,9 @@
+import reducer from '../reducer';
+import thunk from './redux-thunk.js';
+import reporter from './redux-reporter.js';
+import {createStore, applyMiddleware} from 'redux';
+
+let appStoreCreate = () =>
+ createStore(reducer, applyMiddleware(thunk, reporter))
+
+export default appStoreCreate;
diff --git a/frontend/src/lib/redux-reporter.js b/frontend/src/lib/redux-reporter.js
new file mode 100644
index 0000000..0a347af
--- /dev/null
+++ b/frontend/src/lib/redux-reporter.js
@@ -0,0 +1,15 @@
+let reporter = 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;
+ }
+}
+
+export default reporter;
diff --git a/frontend/src/lib/redux-thunk.js b/frontend/src/lib/redux-thunk.js
new file mode 100644
index 0000000..49771bd
--- /dev/null
+++ b/frontend/src/lib/redux-thunk.js
@@ -0,0 +1,4 @@
+export default store => next => action =>
+ typeof action === 'function'
+ ? action(store.dispatch, store.getState)
+ : next(action)
diff --git a/frontend/src/lib/util.js b/frontend/src/lib/util.js
new file mode 100644
index 0000000..71df0dd
--- /dev/null
+++ b/frontend/src/lib/util.js
@@ -0,0 +1,47 @@
+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)
+ }
+ })
+}
+
+// got from:
+// https://stackoverflow.com/questions/14573223/set-cookie-and-get-cookie-with-javascript
+export const readCookie = (name) => {
+ var nameEQ = name + '=';
+ var ca = document.cookie.split(';');
+ for(var i=0; i < ca.length; i++) {
+ var c = ca[i];
+ while(c.charAt(0) == ' ') c = c.substring(1, c.length);
+ if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
+ }
+ return null;
+}
+
+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 => !!options[key]).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);
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 0000000..e59180f
--- /dev/null
+++ b/frontend/src/main.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import ReactDom from 'react-dom';
+import {Provider} from 'react-redux';
+
+import appStoreCreate from './lib/app-create-store.js';
+import App from './component/app';
+
+let store = appStoreCreate();
+console.log('STORE', store.getState());
+let AppContainer = () => {
+ return (
+
+
+
+ )
+}
+
+ReactDom.render(, document.getElementById('root'));
diff --git a/frontend/src/reducer/auth.js b/frontend/src/reducer/auth.js
new file mode 100644
index 0000000..12a3e1c
--- /dev/null
+++ b/frontend/src/reducer/auth.js
@@ -0,0 +1,11 @@
+export default (state=null, action) => {
+ let {type, payload} = action;
+ switch(type) {
+ case 'TOKEN_SET':
+ return payload
+ case 'TOKEN_DELETE':
+ return null
+ default:
+ return state
+ }
+}
diff --git a/frontend/src/reducer/index.js b/frontend/src/reducer/index.js
new file mode 100644
index 0000000..a096f45
--- /dev/null
+++ b/frontend/src/reducer/index.js
@@ -0,0 +1,6 @@
+import {combineReducers} from 'redux';
+import auth from './auth.js';
+
+export default combineReducers({
+ auth,
+})
diff --git a/frontend/src/reducer/photos.js b/frontend/src/reducer/photos.js
new file mode 100644
index 0000000..2571b1c
--- /dev/null
+++ b/frontend/src/reducer/photos.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/frontend/src/reducer/profile.js b/frontend/src/reducer/profile.js
new file mode 100644
index 0000000..e1f735c
--- /dev/null
+++ b/frontend/src/reducer/profile.js
@@ -0,0 +1,21 @@
+let validateProfileCreate = (profile) => {
+ if (!profile.avatar || !profile.bio || !profile._id || !profile.owner || !profile.username || !profile.email) {
+ throw new Error('VALIDATION ERROR: profile requires additional info');
+ }
+}
+
+export default (state=null, action) => {
+ let {type, payload} = action;
+
+ switch(type) {
+ case 'PROFILE_CREATE':
+ validateProfileCreate(payload);
+ return payload;
+ case 'PROFILE_UPDATE':
+ return {...state, ...payload}
+ case 'LOGOUT':
+ return null
+ default:
+ return state
+ }
+}
diff --git a/frontend/src/style/base/_base.scss b/frontend/src/style/base/_base.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/style/base/_reset.scss b/frontend/src/style/base/_reset.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/style/layout/_content.scss b/frontend/src/style/layout/_content.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/style/layout/_footer.scss b/frontend/src/style/layout/_footer.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/style/layout/_header.scss b/frontend/src/style/layout/_header.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/style/lib/_vars.scss b/frontend/src/style/lib/_vars.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/style/main.scss b/frontend/src/style/main.scss
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js
new file mode 100644
index 0000000..e06dfe6
--- /dev/null
+++ b/frontend/webpack.config.js
@@ -0,0 +1,86 @@
+'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');
+
+let plugins = [
+ new EnvironmentPlugin(['NODE_ENV']),
+ new ExtractPlugin('bundle-[hash].css'),
+ new HtmlPlugin({ template: `${__dirname}/src/index.html` }),
+ new DefinePlugin({
+ __DEBUG__: JSON.stringify(!production),
+ __API_URL__: JSON.stringify(process.env.API_URL)
+ })
+]
+
+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: /\.(woff|woff2|ttf|eot|glyph|\.svg)$/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 10000,
+ name: 'font/[name].[ext]'
+ }
+ }
+ ]
+ },
+ {
+ test: /\.(jpg|jpeg|gif|png|tiff|svg)$/,
+ exclude: /\.glyph.svg/,
+ use: [
+ {
+ loader: 'url-loader',
+ options: {
+ limit: 6000,
+ name: 'image/[name].[ext]'
+ }
+ }
+ ]
+ },
+ {
+ test: /\.(mp3|aac|aiff|wav|flac|m4a|mp4|ogg)$/,
+ exclude: /\.glyph.svg/,
+ use: [
+ {
+ loader: 'file-loader',
+ options: { name: 'audio/[name].[ext]' }
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/sluggram b/sluggram
new file mode 160000
index 0000000..1fc6e99
--- /dev/null
+++ b/sluggram
@@ -0,0 +1 @@
+Subproject commit 1fc6e992ae12d84d6e3f8dfc67a18c9f2eca29ad