diff --git a/backend b/backend
new file mode 160000
index 0000000..1fc6e99
--- /dev/null
+++ b/backend
@@ -0,0 +1 @@
+Subproject commit 1fc6e992ae12d84d6e3f8dfc67a18c9f2eca29ad
diff --git a/frontend/.babelrc b/frontend/.babelrc
new file mode 100644
index 0000000..94db7b2
--- /dev/null
+++ b/frontend/.babelrc
@@ -0,0 +1,4 @@
+{
+ "presets": [ "es2015", "react"],
+ "plugins": ["transform-object-rest-spread"]
+}
\ No newline at end of file
diff --git a/frontend/.eslintrc b/frontend/.eslintrc
new file mode 100644
index 0000000..b663d77
--- /dev/null
+++ b/frontend/.eslintrc
@@ -0,0 +1,21 @@
+{
+ "rules": {
+ "no-console": "off",
+ "indent": [ "error", 2 ],
+ "quotes": [ "error", "single" ],
+ "semi": ["error", "always"],
+ "linebreak-style": [ "error", "unix" ]
+ },
+ "env": {
+ "es6": true,
+ "node": true,
+ "mocha": true,
+ "jasmine": true
+ },
+ "ecmaFeatures": {
+ "modules": true,
+ "experimentalObjectRestSpread": true,
+ "impliedStrict": true
+ },
+ "extends": "eslint:recommended"
+}
\ No newline at end of file
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..b512c09
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..4f6366c
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "frontend",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC"
+}
diff --git a/frontend/src/__test__/auth-reducer.test.js b/frontend/src/__test__/auth-reducer.test.js
new file mode 100644
index 0000000..d3c573a
--- /dev/null
+++ b/frontend/src/__test__/auth-reducer.test.js
@@ -0,0 +1,30 @@
+import authReducer from '../reducer/auth.js';
+
+describe('Auth Reducer', () => {
+ let state = {auth: '1234567890987654321'}
+
+ test('initial state should be null', () => {
+ let result = authReducer(undefined, {type: null});
+ expect(result).toEqual(null)
+ })
+
+ test('no action provided should return default state', () => {
+ let result = authReducer(state, {type: null});
+ expect(result).toEqual(state);
+ })
+
+ test('TOKEN_SET should return a token', () => {
+ let action = {
+ type: 'TOKEN_SET',
+ payload: 'sample token'
+ }
+
+ let result = authReducer({}, action);
+ expect(result).toBe(action.payload);
+ })
+
+ test('TOKEN_DELETE should return null', () => {
+ let result = authReducer(state, {type: 'TOKEN_DELETE'});
+ expect(result).toBe(null);
+ })
+})
\ No newline at end of file
diff --git a/frontend/src/action/auth-actions.js b/frontend/src/action/auth-actions.js
new file mode 100644
index 0000000..5487eac
--- /dev/null
+++ b/frontend/src/action/auth-actions.js
@@ -0,0 +1,36 @@
+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.token;
+ } 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;
+ })
+}
\ No newline at end of file
diff --git a/frontend/src/component/app/index.js b/frontend/src/component/app/index.js
new file mode 100644
index 0000000..1c80266
--- /dev/null
+++ b/frontend/src/component/app/index.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import {Provider} from 'react-redux';
+import {BrowserRouter, Route, Link} from 'react-router-dom';
+import appStoreCreate from '../../lib/app-create-store';
+import Landing from '../landing';
+
+let store = appStoreCreate();
+
+class App extends React.Component {
+ render() {
+ return (
+
+ )
+ }
+}
+
+export default App;
\ No newline at end of file
diff --git a/frontend/src/component/auth-form/index.js b/frontend/src/component/auth-form/index.js
new file mode 100644
index 0000000..c58e069
--- /dev/null
+++ b/frontend/src/component/auth-form/index.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import * as util from '../../lib/util.js';
+
+class AuthForm extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state ={
+ username: '',
+ password: '',
+ email: '',
+ 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;
\ No newline at end of file
diff --git a/frontend/src/component/landing/index.js b/frontend/src/component/landing/index.js
new file mode 100644
index 0000000..a1fe9bd
--- /dev/null
+++ b/frontend/src/component/landing/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 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) => {
+ return {
+ signup: (user) => dispatch(signupRequest(user)),
+ login: (user) => dispatch(loginRequest(user))
+ }
+}
+
+export default connect(undefined, mapDispatchToProps)(Landing)
\ No newline at end of file
diff --git a/frontend/src/index.html b/frontend/src/index.html
new file mode 100644
index 0000000..3437ff3
--- /dev/null
+++ b/frontend/src/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ slugram
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/lib/app-create-store.js b/frontend/src/lib/app-create-store.js
new file mode 100644
index 0000000..41baedc
--- /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;
\ No newline at end of file
diff --git a/frontend/src/lib/redux-reporter.js b/frontend/src/lib/redux-reporter.js
new file mode 100644
index 0000000..4996114
--- /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;
\ No newline at end of file
diff --git a/frontend/src/lib/redux-thunk.js b/frontend/src/lib/redux-thunk.js
new file mode 100644
index 0000000..4589400
--- /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)
\ No newline at end of file
diff --git a/frontend/src/lib/util.js b/frontend/src/lib/util.js
new file mode 100644
index 0000000..8c04a24
--- /dev/null
+++ b/frontend/src/lib/util.js
@@ -0,0 +1,19 @@
+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);
\ No newline at end of file
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 0000000..d209467
--- /dev/null
+++ b/frontend/src/main.js
@@ -0,0 +1,5 @@
+import React from 'react';
+import ReactDom from 'react-dom';
+import App from './component/app';
+
+ReactDom.render(, document.getElementById('root'));
\ No newline at end of file
diff --git a/frontend/src/reducer/auth.js b/frontend/src/reducer/auth.js
new file mode 100644
index 0000000..7ceb891
--- /dev/null
+++ b/frontend/src/reducer/auth.js
@@ -0,0 +1,12 @@
+export default (state=null, action) => {
+ let {type, payload} = action;
+
+ switch (type) {
+ case 'TOKEN_SET':
+ return payload;
+ case 'TOKEN_DELETE':
+ return null
+ default:
+ return state;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/reducer/index.js b/frontend/src/reducer/index.js
new file mode 100644
index 0000000..28712ec
--- /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,
+})
\ No newline at end of file
diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js
new file mode 100644
index 0000000..ab98d5f
--- /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]' }
+ }
+ ]
+ }
+ ]
+ }
+};
\ No newline at end of file