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 ( +
+ + +
+
+

SLuGrAM

+ +
+ +
+
+
+
+ ) + } +} + +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 ( +
+ + {util.renderIf(this.props.auth === 'signup', + + )} + + {util.renderIf(this.state.usernameError, + + {this.state.usernameError} + + )} + + + + {util.renderIf(this.state.passwordError, + + {this.state.passwordError} + + )} + + + + +
+ ) + } +} + +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