+ );
+ }
+}
diff --git a/src/components/Layout/index.js b/src/components/Layout/index.js
new file mode 100644
index 0000000..5bc5a2b
--- /dev/null
+++ b/src/components/Layout/index.js
@@ -0,0 +1,3 @@
+import Layout from './Layout';
+
+export default Layout;
diff --git a/src/components/Main/Main.jsx b/src/components/Main/Main.jsx
new file mode 100644
index 0000000..033e242
--- /dev/null
+++ b/src/components/Main/Main.jsx
@@ -0,0 +1,31 @@
+import React, { createElement } from 'react';
+import { routeNode } from 'react-router5';
+
+import Home from '../../pages/Home';
+import Index from '../../pages/Index';
+import Login from '../../pages/Login';
+
+
+const components = {
+ 'home': Home,
+ 'index': Index,
+ 'login': Login,
+};
+
+class Main extends React.Component {
+
+ render(){
+ const { route } = this.props;
+ const segment = route.name.split('.')[0];
+
+ return createElement(components[segment],
+ { ...route.params }
+ // For full unmount/mount pass the key as below:
+ // { key: route.meta.id, ...route.params }
+ );
+ }
+}
+// withRouter is a High Order Function
+// It is a sort of decorator to augment the Component class with extra methods or properties coming from react-router
+// For example we can now access "this.props.router" (we need listenersPlugin from router5)
+export default routeNode('')(Main); // '' because it it the root node
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..502303e
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,11 @@
+import axios from 'axios';
+
+// GLOBAL AXIOS CONFIG
+axios.defaults.baseURL = '/api';
+axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
+axios.defaults.xsrfCookieName = 'csrftoken';
+axios.defaults.xsrfHeaderName = 'X-CSRFToken';
+
+module.exports = {}
+
+
diff --git a/src/create-router5.js b/src/create-router5.js
new file mode 100644
index 0000000..913059a
--- /dev/null
+++ b/src/create-router5.js
@@ -0,0 +1,29 @@
+import createRouter from 'router5';
+import loggerPlugin from 'router5/plugins/logger';
+import listenersPlugin from 'router5/plugins/listeners';
+import browserPlugin from 'router5/plugins/browser';
+import routes from './routes';
+
+
+const routerOptions = {
+ defaultRoute: 'home',
+ strictQueryParams: true
+};
+
+function configureRouter(useListenersPlugin = false) {
+ const router = createRouter(routes, routerOptions)
+ // Plugins
+ .usePlugin(loggerPlugin)
+ .usePlugin(browserPlugin({
+ useHash: true
+ }));
+
+ if (useListenersPlugin) {
+ router.usePlugin(listenersPlugin());
+ }
+
+ return router;
+}
+
+// I can import the default module with whatever name I want (createRouter)
+export default configureRouter;
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
new file mode 100644
index 0000000..743c50f
--- /dev/null
+++ b/src/pages/Home.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import styles from './Home.sass';
+import { routeNode } from 'react-router5';
+
+class Home extends React.Component {
+ render() {
+ return (
+
+
Home, I am a subroute page
+
+
+ );
+ }
+}
+
+
+export default routeNode('home')(Home);
diff --git a/src/pages/Home.sass b/src/pages/Home.sass
new file mode 100644
index 0000000..0306c59
--- /dev/null
+++ b/src/pages/Home.sass
@@ -0,0 +1,10 @@
+.redBg
+ background-color: red
+ border-radius: 5px
+ color: white
+ padding: 5px
+
+
+.container-border
+ border: 1px dashed grey
+
diff --git a/src/pages/Index.jsx b/src/pages/Index.jsx
new file mode 100644
index 0000000..2fa7fa6
--- /dev/null
+++ b/src/pages/Index.jsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { observer, inject} from 'mobx-react';
+import styles from './Index.sass';
+import { routeNode } from 'react-router5';
+
+@inject("tabStore")
+@observer
+class Index extends React.Component {
+
+ _setActiveTab() {
+ const router = this.props.router;
+ const route = this.props.route;
+ const tabStore = this.props.tabStore;
+ const { id } = route.params;
+ tabStore.setActiveTab(id);
+ }
+
+ constructor(props) {
+ super(props);
+ }
+
+ // Triggered when a component will be scheduled to re-render because data it observes (from mobx store) has changed.
+ // This makes it easy to trace renders back to the action that caused the rendering.
+ componentWillReact() {
+ console.log("I will re-render, since the todo has changed!");
+ }
+
+
+ // componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here. I
+ // If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
+ // Setting state in this method will trigger a re-rendering.
+ componentDidMount() {
+ console.log("I did mount Index");
+ this._setActiveTab();
+ }
+
+ componentWillUnmount(){
+ console.log("I will unmount Index");
+ }
+
+ // This method is not called for the initial render.
+ componentDidUpdate(prevProps, prevState) {
+ this._setActiveTab();
+ console.log("I will re-render for whatever reason");
+ }
+
+ render() {
+ const tabStore = this.props.tabStore;
+ return (
+
+
Index, I am The Index Page {tabStore.activeTab}
+
This is some computed values:
+
+
+ Tabs Number: {tabStore.tabNumbers}
+
+
+
{JSON.stringify(tabStore.tabs)}
+
+ );
+ }
+}
+
+export default routeNode('index')(Index);
diff --git a/src/pages/Index.sass b/src/pages/Index.sass
new file mode 100644
index 0000000..f0d0927
--- /dev/null
+++ b/src/pages/Index.sass
@@ -0,0 +1,10 @@
+.redBg
+ background-color: red
+ border-radius: 5px
+ color: white
+ padding: 5px
+
+
+.container-border
+ border: 1px dashed grey
+
diff --git a/src/pages/Login.jsx b/src/pages/Login.jsx
new file mode 100644
index 0000000..b96ab06
--- /dev/null
+++ b/src/pages/Login.jsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import {login, logout} from '../actions/auth'
+import { observer, inject } from 'mobx-react';
+import { routeNode } from 'react-router5';
+
+// 'inject' can be used to pick up the stores passed to components with Provider.
+// It is a HOC that takes a list of strings and makes those stores available as 'this.props.storeName' to the wrapped component.
+@inject('userStore')
+@observer
+class Login extends React.Component {
+
+ // To validate the injected store
+ static propTypes = {
+ userStore: React.PropTypes.object.isRequired
+ };
+
+ constructor(props){
+ super(props);
+ this.formData = {email:"", password:""};
+ // BINDING
+ this.onEmailChange = this.onEmailChange.bind(this);
+ this.onPasswordChange = this.onPasswordChange.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ this.onLogout = this.onLogout.bind(this);
+ }
+
+ componentDidMount(){
+ console.log("I did mount the login");
+ }
+
+ componentWillUnmount(){
+ console.log("I will unmount the login");
+ }
+
+ onEmailChange(e) {
+ Object.assign(this.formData, {}, {email: e.target.value});
+ console.log(this.formData);
+ }
+
+ onPasswordChange(e) {
+ Object.assign(this.formData, {}, {password: e.target.value});
+ console.log(this.formData);
+ }
+
+ onSubmit(event) {
+ event.preventDefault();
+ login(this.formData);
+ }
+
+ onLogout(e) {
+ event.preventDefault();
+ logout();
+ }
+
+ render() {
+ return (
+
+
{JSON.stringify(this.props.userStore.user)}
+
+
Login
+
+
+
+
+
+
+ );
+ }
+}
+
+export default routeNode('login')(Login);
diff --git a/src/routes.js b/src/routes.js
new file mode 100644
index 0000000..7766483
--- /dev/null
+++ b/src/routes.js
@@ -0,0 +1,5 @@
+export default [
+ { name: 'home', path: '/' },
+ { name: 'login', path: '/login'},
+ { name: 'index', path: '/index/:id'},
+];
diff --git a/src/services/api.js b/src/services/api.js
new file mode 100644
index 0000000..70b786d
--- /dev/null
+++ b/src/services/api.js
@@ -0,0 +1 @@
+// TODO
diff --git a/src/stores/TabStore.js b/src/stores/TabStore.js
new file mode 100644
index 0000000..f261963
--- /dev/null
+++ b/src/stores/TabStore.js
@@ -0,0 +1,48 @@
+import { observable, computed, autorun, action } from 'mobx';
+
+// Mobx Observable Store
+class TabStore {
+
+ constructor() {
+ // Autorun runs every time the store changes
+ // In reality what I want is to run a react .render() whenever something changes
+ autorun(() => console.log("Active Tab: " + this.activeTab));
+ }
+
+ @observable activeTab = null;
+ @observable tabs = [];
+
+ @computed get tabNumbers() {
+ return this.tabs.length;
+ }
+
+
+ // ===========
+ // = Methods =
+ // ===========
+ _addTab = (id) => {
+
+ const found = this.tabs.find(function(item, index, array) {
+ return item.id === id
+ });
+ if (!found) {
+ this.tabs.push({
+ id: id,
+ });
+ }
+ else {
+ console.log("Element already in array");
+ }
+ };
+
+ @action setActiveTab = (id) => {
+ this.activeTab = id;
+ this._addTab(id);
+ };
+
+
+}
+
+
+const tabStore = window.__tabStore__ = new TabStore();
+export default tabStore;
diff --git a/src/stores/UserStore.js b/src/stores/UserStore.js
new file mode 100644
index 0000000..b149612
--- /dev/null
+++ b/src/stores/UserStore.js
@@ -0,0 +1,57 @@
+import { observable, action, computed } from 'mobx';
+
+// TODO:
+function _handleErrors(response) {
+ if (response.data && response.data.success !== true) {
+ throw Error(response);
+ }
+ return response;
+}
+
+
+class UserStore {
+
+ constructor() {
+ this.user = this.defaultUser;
+ this.watchlist = [];
+ }
+
+
+ defaultUser = {
+ "level" : "V",
+ "status" : "U"
+ };
+
+ // ===============
+ // = OBSERVABLES =
+ // ===============
+ @observable user;
+ @observable watchlist;
+
+ // ============
+ // = COMPUTED =
+ // ============
+ @computed get isLoggedIn() {
+ return this.user.level !== 'V';
+ }
+
+ // ===========
+ // = Actions =
+ // ===========
+ @action setUser = (user) => {
+ this.user = user;
+ };
+
+ @action resetUser = () => {
+ this.user = this.defaultUser;
+ this.watchlist = [];
+ };
+
+}
+
+
+const userStore = new UserStore();
+
+export default userStore;
+
+export { UserStore };
diff --git a/src/stores/index.js b/src/stores/index.js
new file mode 100644
index 0000000..43fe932
--- /dev/null
+++ b/src/stores/index.js
@@ -0,0 +1,8 @@
+import tabStore from './TabStore';
+import userStore from './UserStore';
+
+export {
+ tabStore,
+ userStore,
+};
+
diff --git a/src/styles/abstracts/_mixins.sass b/src/styles/abstracts/_mixins.sass
new file mode 100644
index 0000000..c179c02
--- /dev/null
+++ b/src/styles/abstracts/_mixins.sass
@@ -0,0 +1,31 @@
+=container
+ margin: auto
+ width: $width
+
+=clearfix
+ &:after
+ content: ""
+ display: table
+ clear: both
+
+// ==================
+// = FONT-SMOOTHING =
+// ==================
+// Activate/Deactivate font-smoothing
+// The default is subpixel level, by passing "on" we deactivate it, activating the antialiased (less defined) level.
+// This is needed for light text on dark bg `bug` on chrome: http://usabilitypost.com/2012/11/05/stop-fixing-font-smoothing/
+=font-smoothing-antialiased($value: on)
+ @if $value == on
+ -webkit-font-smoothing: antialiased // DO NOT USE THIS SITE-WIDE
+ -moz-osx-font-smoothing: grayscale
+ @else
+ -webkit-font-smoothing: subpixel-antialiased // On most non-retina displays this will give the sharpest text (default)
+ -moz-osx-font-smoothing: auto
+
+
+// It removes the subpixel smoothing (default) for light text on dark bg
+=light-on-dark
+ +font-smoothing-antialiased(on)
+
+=dark-on-light
+ +font-smoothing-antialiased(off)
diff --git a/src/styles/abstracts/_variables.sass b/src/styles/abstracts/_variables.sass
new file mode 100644
index 0000000..918bb0e
--- /dev/null
+++ b/src/styles/abstracts/_variables.sass
@@ -0,0 +1,27 @@
+
+$width : 960px
+
+// Colors
+$redSB : #87000D
+$redSB-light : #A90010
+$redSB-light2 : #98000F
+$redSB-dark : #640009
+$redSB-dark2: #75000A
+$grey94 : #F1F1F1
+$orange: rgb(200, 42, 30)
+// Fonts
+$mainFont : "myriad-pro"
+$fallBackFonts : "Myriad Pro", MyriadPro, Arial, sans-serif
+
+// ===========
+// = Z-Index =
+// ===========
+$zindex_headerContainer: 11
+
+$zindex_navAccount: 14
+$zindex_navAccount__Dropdown: 13
+$zindex_socialbuttons: 12
+
+$zindex_menuNavigation: 13
+$zindex_menuNavigationTooltips: 10
+$zindex_menuNavigationDropdown: 10
diff --git a/src/styles/base/_commons.sass b/src/styles/base/_commons.sass
new file mode 100644
index 0000000..0898aaa
--- /dev/null
+++ b/src/styles/base/_commons.sass
@@ -0,0 +1,13 @@
+// Display the list form left to right
+
+ul
+ horizontal
+ +clearfix
+ li
+ float: left
+
+
+.clearfix
+ +clearfix
+
+
diff --git a/src/styles/base/_reset.sass b/src/styles/base/_reset.sass
new file mode 100644
index 0000000..5f8c6f4
--- /dev/null
+++ b/src/styles/base/_reset.sass
@@ -0,0 +1,61 @@
+/* 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
+
+a
+ text-decoration: none
+ outline: 0
+
+
+/* apply a natural box layout model to all elements */
+*, *:before, *:after
+ -moz-box-sizing: border-box
+ -webkit-box-sizing: border-box
+ box-sizing: border-box
+
+
+html
+ font-family: sans-serif
\ No newline at end of file
diff --git a/src/template.html b/src/template.html
new file mode 100644
index 0000000..d743bc1
--- /dev/null
+++ b/src/template.html
@@ -0,0 +1,15 @@
+
+
+
+
+ React App
+
+
+
+
+
Loading...
+
+
+
+
+
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..2782ed5
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,64 @@
+"use strict";
+var webpack = require('webpack');
+var path = require('path');
+var loadersConf = require('./webpack.loaders');
+var DashboardPlugin = require('webpack-dashboard/plugin');
+
+const HOST = process.env.HOST || "127.0.0.1";
+const PORT = process.env.PORT || "8888";
+
+module.exports = {
+ // If you pass an array: All modules are loaded upon startup. The last one is exported.
+ entry: [
+ 'babel-polyfill', // emulate a full ES2015 (Promise, WeakMap, ectr)
+ 'react-hot-loader/patch', // needed for hot loader v3
+ './src/app.jsx' // your app's entry point (exported module)
+ ],
+ output: {
+ path: path.join(__dirname, 'public'),
+ filename: 'bundle.js',
+ publicPath: 'http://localhost:8888/public/',
+ },
+ module: {
+ loaders: loadersConf
+ },
+ resolve: {
+ extensions: ['', '.js', '.jsx'],
+ },
+ devtool: '#inline-source-map', // 'eval-cheap-source-map',
+ devServer: {
+ contentBase: "./",
+ publicPath: "http://localhost:8888/public/",
+ // do not print bundle build stats
+ noInfo: true,
+ // enable HMR
+ hot: true,
+ // embed the webpack-dev-server runtime into the bundle
+ inline: true,
+ // serve index.html in place of 404 responses to allow HTML5 history
+ historyApiFallback: true,
+ port: PORT,
+ host: HOST,
+ colors: true,
+ // proxy to my local dev server running at 8000 port for anything starting with 'api' (then stripped)
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8000',
+ pathRewrite: {'^/api' : ''},
+ secure: false,
+ changeOrigin: true,
+ logLevel: 'debug',
+ }
+ }
+ },
+ // These files will imported in every sass file (imported)
+ sassResources: [
+ './src/styles/abstracts/_variables.sass',
+ './src/styles/abstracts/_mixins.sass',
+ ],
+ plugins: [
+ new webpack.NoErrorsPlugin(),
+ new webpack.HotModuleReplacementPlugin(),
+ new DashboardPlugin(), // cool console output
+ ]
+};
diff --git a/webpack.loaders.js b/webpack.loaders.js
new file mode 100644
index 0000000..61fd9d8
--- /dev/null
+++ b/webpack.loaders.js
@@ -0,0 +1,97 @@
+var path = require('path');
+
+// Modularized sass files
+const sassLoaders = [
+ 'css-loader?sourceMap&camelCase=dashes&modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
+ 'postcss-loader?sourceMap=inline',
+ 'sass-loader?sourceMap&outputStyle=expanded&indentedSyntax=sass&includePaths[]=' + path.resolve(__dirname, './src/styles'),
+ 'sass-resources' // NB: the config for sass-resources is in the the webpack config file
+];
+
+// Not modularize sass files (globals)
+const sassLoadersGloabals = [
+ 'css-loader?sourceMap&camelCase=dashes&importLoaders=1',
+ 'postcss-loader?sourceMap=inline',
+ 'sass-loader?sourceMap&outputStyle=expanded&indentedSyntax=sass&includePaths[]=' + path.resolve(__dirname, './src/styles'),
+ 'sass-resources' // NB: the config for sass-resources is in the the webpack config file
+];
+
+
+module.exports = [
+ // =========
+ // = Babel =
+ // =========
+ // Load these exts with babel (so we can use 'import' instead of 'require' and es6 syntax)
+ {
+ test: /\.jsx?$/,
+ include: __dirname + '/src/',
+ loader: "babel"
+ },
+ // =========
+ // = Fonts =
+ // =========
+ {
+ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
+ exclude: /(node_modules)/,
+ loader: "file"
+ },
+ {
+ test: /\.(woff|woff2)$/,
+ exclude: /(node_modules)/,
+ loader: "url?prefix=font/&limit=5000"
+ },
+ {
+ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
+ exclude: /(node_modules)/,
+ loader: "url?limit=10000&mimetype=application/octet-stream"
+ },
+ // ==========
+ // = Images =
+ // ==========
+ {
+ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
+ exclude: /(node_modules)/,
+ loader: "url?limit=10000&mimetype=image/svg+xml"
+ },
+ {
+ test: /\.gif/,
+ exclude: /(node_modules)/,
+ loader: "url-loader?limit=10000&mimetype=image/gif"
+ },
+ {
+ test: /\.jpg/,
+ exclude: /(node_modules)/,
+ loader: "url-loader?limit=10000&mimetype=image/jpg"
+ },
+ {
+ test: /\.png/,
+ exclude: /(node_modules)/,
+ loader: "url-loader?limit=10000&mimetype=image/png&name=[path][name].[ext]"
+ },
+ // ==========
+ // = Styles =
+ // ==========
+ // NB:
+ // - css-loader takes a CSS file and reads off all its dependencies
+ // - style-loader will embed those styles directly into the markup(not when using dev-server)
+
+ // Global CSS (from node_modules)
+ {
+ test: /\.css$/,
+ include: __dirname + '/node_modules',
+ loader: 'style-loader!css-loader'
+ },
+ // Global ('locals') sass imports. Do not modularize these imports (leave them as global css styles)
+ {
+ test: /\.(sass|scss)$/,
+ include: __dirname + '/src/styles/base',
+ loader: 'style-loader!' + sassLoadersGloabals.join('!')
+ },
+ // Local SASS modules
+ {
+ test: /\.(sass|scss)$/,
+ exclude: __dirname + '/src/styles/base',
+ loader: 'style-loader!' + sassLoaders.join('!')
+ },
+];
+
diff --git a/webpack.production.config.js b/webpack.production.config.js
new file mode 100644
index 0000000..dbfe111
--- /dev/null
+++ b/webpack.production.config.js
@@ -0,0 +1,69 @@
+
+// TODO
+
+var webpack = require('webpack');
+var path = require('path');
+var loaders = require('./webpack.loaders');
+var ExtractTextPlugin = require('extract-text-webpack-plugin');
+var HtmlWebpackPlugin = require('html-webpack-plugin');
+var WebpackCleanupPlugin = require('webpack-cleanup-plugin');
+
+// local css modules
+loaders.push({
+ test: /[\/\\]src[\/\\].*\.css/,
+ exclude: /(node_modules|bower_components|public)/,
+ loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]')
+});
+
+// local scss modules
+loaders.push({
+ test: /[\/\\]src[\/\\].*\.scss/,
+ exclude: /(node_modules|bower_components|public)/,
+ loader: ExtractTextPlugin.extract('style', 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass')
+});
+// global css files
+loaders.push({
+ test: /[\/\\](node_modules|global)[\/\\].*\.css$/,
+ loader: ExtractTextPlugin.extract('style', 'css')
+});
+
+module.exports = {
+ entry: [
+ './src/app.jsx'
+ ],
+ output: {
+ path: path.join(__dirname, 'public'),
+ filename: '[chunkhash].js'
+ },
+ resolve: {
+ extensions: ['', '.js', '.jsx']
+ },
+ module: {
+ loaders
+ },
+ plugins: [
+ new WebpackCleanupPlugin(),
+ new webpack.DefinePlugin({
+ 'process.env': {
+ NODE_ENV: '"production"'
+ }
+ }),
+ new webpack.optimize.UglifyJsPlugin({
+ compress: {
+ warnings: false,
+ screw_ie8: true,
+ drop_console: true,
+ drop_debugger: true
+ }
+ }),
+ new webpack.optimize.OccurenceOrderPlugin(),
+ new ExtractTextPlugin('[contenthash].css', {
+ allChunks: true
+ }),
+ new HtmlWebpackPlugin({
+ template: './src/template.html',
+ title: 'Webpack App'
+ }),
+ new webpack.optimize.DedupePlugin()
+ ]
+};