Initial commit

This commit is contained in:
Jori Lallo 2016-02-27 13:53:11 -08:00
commit af30485e9f
37 changed files with 1135 additions and 0 deletions

8
.babelrc Normal file
View File

@ -0,0 +1,8 @@
{
"presets": ["react", "es2015", "stage-0"],
"env": {
"development": {
"presets": ["react-hmre"]
}
}
}

9
.eslintrc Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "airbnb",
"parser": "babel-eslint",
"rules": {
"arrow-body-style":[0, "as-needed"], // fix `this` shortcut on ES6 classes
"react/jsx-no-bind": 0, // Makes difficult to pass args to prop functions
"no-else-return": 0,
}
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/*
dist/*
.env
npm-debug.log
.DS_Store

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Beautiful Atlas
## Ideas
- Create sharable private URLs for notes
- Settings
- Enable :emoji: autoconvert

22
index.html Normal file
View File

@ -0,0 +1,22 @@
<!doctype html>
<html>
<head>
<title>Beautiful Atlas</title>
<link href='https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.11.0/codemirror.min.css' rel='stylesheet'>
<style type="text/css">
html, body, .viewport {
width: 100%;
height: 100%;
margin: 0;
}
.Codemirror {
height: auto;
}
</style>
</head>
<body style='display: flex; width: 100%'>
<div id="root" style='display: flex; width: 100%'></div>
<script src="/static/bundle.js"></script>
</body>
</html>

67
package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "BeautifulAtlas",
"version": "0.0.1",
"description": "For writing",
"main": "index.js",
"scripts": {
"clean": "rimraf dist",
"build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress --colors",
"build": "npm run clean && npm run build:webpack",
"start": "node server.js",
"lint": "eslint src"
},
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/jorilallo/atlas.git"
},
"author": "Jori Lallo",
"license": "ISC",
"bugs": {
"url": "https://github.com/jorilallo/atlas/issues"
},
"homepage": "https://github.com/jorilallo/atlas#readme",
"dependencies": {
"express": "^4.13.4",
"react": "^0.14.7",
"react-dom": "^0.14.7"
},
"devDependencies": {
"babel-core": "^6.4.5",
"babel-eslint": "^4.1.8",
"babel-loader": "^6.2.1",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babel-preset-react-hmre": "^1.0.1",
"babel-preset-stage-0": "^6.5.0",
"body-parser": "^1.15.0",
"codemirror": "^5.11.0",
"cross-env": "^1.0.7",
"css-loader": "^0.23.1",
"dotenv": "^2.0.0",
"eslint": "^1.10.3",
"eslint-config-airbnb": "^5.0.0",
"eslint-plugin-react": "^3.16.1",
"exports-loader": "^0.6.3",
"fetch": "^1.0.1",
"history": "^1.17.0",
"imports-loader": "^0.6.5",
"json-loader": "^0.5.4",
"lodash": "^4.3.0",
"marked": "^0.3.5",
"node-sass": "^3.4.2",
"normalize.css": "^3.0.3",
"react": "^0.14.7",
"react-codemirror": "^0.2.5",
"react-medium-editor": "^1.6.2",
"react-redux": "^4.4.0",
"react-router": "^2.0.0",
"redux": "^3.3.1",
"sass-loader": "^3.1.2",
"style-loader": "^0.13.0",
"to-markdown": "^2.0.1",
"webpack": "^1.12.12",
"webpack-dev-middleware": "^1.5.1",
"webpack-hot-middleware": "^2.6.4",
"whatwg-fetch": "^0.11.0"
}
}

83
server.js Normal file
View File

@ -0,0 +1,83 @@
var path = require('path');
var express = require('express');
var app = express();
var port = process.env.PORT || 3000;
if (process.env.NODE_ENV !== 'production') {
var webpack = require('webpack');
var config = require('./webpack.config.dev');
var compiler = webpack(config);
app.use(require('webpack-dev-middleware')(compiler, {
noInfo: true,
publicPath: config.output.publicPath
}));
app.use(require('webpack-hot-middleware')(compiler));
} else {
app.use('/static', express.static('dist'));
}
// API stubs - Feel free to tear these down in favor of rolling out proper APIs
// Also `body-parser` module is included only for this
var router = express.Router();
var validJwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
function isAuthenticated(req, res, next) {
// Authenticate with JWT
if (req.headers.authorization) {
var tokenParts = req.headers.authorization.split(" ");
if (tokenParts.length === 2 &&
tokenParts[0].trim().toUpperCase() === "JWT" &&
tokenParts[1].trim() === validJwtToken
) {
return next();
}
}
// Return 401 with invalid credentials
res.status(401).json({
'error': 'Invalid JWT token'
});
}
router.post('/authenticate', function(req, res) {
if (req.body.email === 'user1@example.com' &&
req.body.password === 'test123!') {
res.json({
'jwt_token': validJwtToken,
});
} else {
res.status(400).json({
'error': 'Invalid credentials'
});
}
});
router.get('/user', isAuthenticated, function(req, res) {
res.json({
id: '93c3a6d6-3958-44c9-a668-59711befb25c',
email: 'user1@example.com',
name: 'Test User'
});
});
// Register API
var bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use('/api', router);
// Frontend
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname, 'index.html'));
});
app.listen(port, function(err) {
if (err) {
console.log(err);
return;
}
console.log('Listening at ' + port);
});

29
src/Actions/index.js Normal file
View File

@ -0,0 +1,29 @@
import keyMirror from 'fbjs/lib/keyMirror';
/*
* Action types
*/
export const UPDATE_TEXT = 'UPDATE_TEXT';
export const TOGGLE_EDITORS = 'TOGGLE_EDITORS';
/*
* Other Constants
*/
export const ActiveEditors = keyMirror({
MARKDOWN: null,
TEXT: null,
});
/*
* Action creators
*/
export function updateText(text, editor) {
return { type: UPDATE_TEXT, text, editor };
}
export function toggleEditors(toggledEditor) {
return { type: TOGGLE_EDITORS, toggledEditor };
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import styles from './Header.scss';
const Header = ({ activeEditors, toggleEditors }) => {
return (
<div className={ styles.header }>
<div className={ styles.headerItem }><i>Beautiful</i> Atlas</div>
<div className={ `${styles.headerItem} ${styles.editorToggle}` }>
<span
onClick={toggleEditors.bind(this, 'MARKDOWN')}
className={ activeEditors.includes('MARKDOWN') ? styles.active : '' }
>Markdown</span>
<span
onClick={toggleEditors.bind(this, 'TEXT')}
className={ activeEditors.includes('TEXT') ? styles.active : '' }
>Text</span>
</div>
<div className={ styles.headerItem }>Versions</div>
</div>
);
};
export default Header;

View File

@ -0,0 +1,47 @@
.header {
display: flex;
width: 100%;
height: 42px;
justify-content: space-between;
background-color: #111;
color: #fff;
i {
color: #fff;
font-family: serif;
}
.headerItem {
width: 150px;
padding: 12px 22px;
font-size: 13px;
font-weight: 300;
text-align: center;
&:first-child {
text-align: left;
}
&:last-child {
text-align: right;
}
}
.editorToggle {
span {
margin-right: 12px;
cursor: pointer;
&:last-child {
margin-right: 0;
}
}
}
.active {
text-decoration: underline;
text-decoration-color: #fff;
}
}

View File

@ -0,0 +1,2 @@
import Header from './Header';
export default Header;

View File

@ -0,0 +1,52 @@
import React from 'react';
import Codemirror from 'react-codemirror';
import 'codemirror/mode/gfm/gfm';
import 'codemirror/addon/edit/continuelist';
import styles from './MarkdownEditor.scss';
import './codemirror.css';
class MarkdownAtlas extends React.Component {
static propTypes = {
text: React.PropTypes.string,
onChange: React.PropTypes.func,
}
onChange = (newText) => {
if (newText !== this.props.text) {
this.props.onChange(newText);
}
}
render = () => {
// https://github.com/jbt/markdown-editor/blob/master/index.html
const options = {
readOnly: false,
lineNumbers: false,
mode: 'gfm',
matchBrackets: true,
lineWrapping: true,
viewportMargin: Infinity,
theme: 'atlas',
extraKeys: {
Enter: 'newlineAndIndentContinueMarkdownList',
},
};
// http://codepen.io/lubelski/pen/fnGae
// TODO:
// - Emojify
// -
return (
<div className={ styles.container }>
<Codemirror
value={this.props.text}
onChange={this.onChange}
options={options}
/>
</div>
);
}
}
export default MarkdownAtlas;

View File

@ -0,0 +1,4 @@
.container {
width: 70%;
margin: 48px auto;
}

View File

@ -0,0 +1,58 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
@import url(https://fonts.googleapis.com/css?family=Cousine:400,700,700italic,400italic);
.cm-s-atlas.CodeMirror {
background: #fcfcfc;
color: #202020;
font-family: 'Cousine', 'Monaco', monospace;
font-weight: 300;
}
.cm-s-atlas div.CodeMirror-selected {
background: #90CAF9;
}
.cm-s-atlas .CodeMirror-line::selection,
.cm-s-atlas .CodeMirror-line > span::selection,
.cm-s-atlas .CodeMirror-line > span > span::selection {
background: #90CAF9;
}
.cm-s-atlas .CodeMirror-line::-moz-selection, .cm-s-atlas .CodeMirror-line > span::-moz-selection, .cm-s-atlas .CodeMirror-line > span > span::-moz-selection { background: #e0e0e0; }
.cm-s-atlas .CodeMirror-gutters { background: #f5f5f5; border-right: 0px; }
.cm-s-atlas .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-atlas .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-linenumber { color: #b0b0b0; }
.cm-s-atlas .CodeMirror-cursor {
border-left: 2px solid #2196F3;
}
.cm-s-atlas span.cm-quote {
font-style: italic;
}
.cm-s-atlas span.cm-comment { color: #8f5536; }
.cm-s-atlas span.cm-atom { color: #aa759f; }
.cm-s-atlas span.cm-number { color: #aa759f; }
.cm-s-atlas span.cm-property, .cm-s-atlas span.cm-attribute { color: #90a959; }
.cm-s-atlas span.cm-keyword { color: #ac4142; }
.cm-s-atlas span.cm-string { color: #f4bf75; }
.cm-s-atlas span.cm-variable { color: #90a959; }
.cm-s-atlas span.cm-variable-2 { color: #788696; }
.cm-s-atlas span.cm-def { color: #d28445; }
.cm-s-atlas span.cm-bracket { color: #202020; }
.cm-s-atlas span.cm-tag { color: #ac4142; }
.cm-s-atlas span.cm-link { color: #aa759f; }
.cm-s-atlas span.cm-error { background: #ac4142; color: #505050; }
.cm-s-atlas .CodeMirror-activeline-background { background: #DDDCDC; }
.cm-s-atlas .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }

View File

@ -0,0 +1,2 @@
import MarkdownEditor from './MarkdownEditor';
export default MarkdownEditor;

View File

@ -0,0 +1,52 @@
import React from 'react';
import Editor from 'react-medium-editor';
import marked from 'marked';
require('medium-editor/dist/css/medium-editor.css');
require('medium-editor/dist/css/themes/default.css');
import styles from './TextEditor.scss';
class TextEditor extends React.Component {
static propTypes = {
text: React.PropTypes.string,
onChange: React.PropTypes.func,
}
onChange = (newText) => {
if (newText !== this.props.text) {
this.props.onChange(newText);
}
}
render = () => {
return (
<div className={ styles.container }>
<div></div>
<Editor
options={{
toolbar: {
buttons: [
'bold',
'italic',
'underline',
'anchor',
'unorderedlist',
'orderedlist',
'h1',
'h2',
'h3',
'quote',
],
},
placeholder: false,
}}
text={marked(this.props.text)}
onChange={ this.onChange }
className={ styles.editor }
/>
</div>
);
}
}
export default TextEditor;

View File

@ -0,0 +1,5 @@
.editor {
outline: none;
margin: 0 0 20px 0;
padding: 0 0 20px 0;
}

View File

@ -0,0 +1,2 @@
import TextEditor from './TextEditor';
export default TextEditor;

19
src/Constants.js Normal file
View File

@ -0,0 +1,19 @@
import keyMirror from 'fbjs/lib/keyMirror';
// Get application version from package.json 😅
import { name, version } from '../package.json';
// Constant KEYS 🔑
const keys = keyMirror({
JWT_STORE_KEY: null, // localStorage key for JWT
});
// Constant values
const constants = {
API_USER_AGENT: `${name}/${version}`,
API_BASE_URL: 'http://localhost:3000/api',
LOGIN_PATH: '/login',
LOGIN_SUCCESS_PATH: '/dashboard',
};
export default Object.assign(keys, constants);

39
src/Reducers/index.js Normal file
View File

@ -0,0 +1,39 @@
import _ from 'lodash';
import { combineReducers } from 'redux';
import {
UPDATE_TEXT,
TOGGLE_EDITORS,
ActiveEditors,
} from '../Actions';
function activeEditors(state = [ActiveEditors.MARKDOWN, ActiveEditors.TEXT], action) {
switch (action.type) {
case TOGGLE_EDITORS: {
const newState = _.xor(state, [action.toggledEditor]);
if (newState.length > 0) {
return newState;
} else {
return [action.toggledEditor];
}
}
default:
return state;
}
}
function text(state = '', action) {
switch (action.type) {
case UPDATE_TEXT:
return action.text;
default:
return state;
}
}
const application = combineReducers({
activeEditors,
text,
});
export default application;

109
src/Utils/ApiClient.js Normal file
View File

@ -0,0 +1,109 @@
import _ from 'lodash';
import Auth from './Auth';
import Constants from '../Constants';
class ApiClient {
constructor(options = {}) {
this.baseUrl = options.baseUrl || Constants.API_BASE_URL;
this.userAgent = options.userAgent || Constants.API_USER_AGENT;
}
fetch = (path, method, data) => {
let body;
let modifiedPath;
if (method === 'GET') {
modifiedPath = path + this.constructQueryString(data);
} else if (method === 'POST' || method === 'PUT') {
body = JSON.stringify(data);
}
// Construct headers
const headers = new Headers({
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.userAgent,
});
if (Auth.getToken()) {
headers.set('Authorization', `JWT ${Auth.getToken()}`);
}
// Construct request
const request = fetch(this.baseUrl + (modifiedPath || path), {
method,
body,
headers,
redirect: 'follow',
});
// Handle request promises and return a new promise
return new Promise((resolve, reject) => {
request
.then((response) => {
// Handle successful responses
if (response.status >= 200 && response.status < 300) {
return response;
}
// Handle 401, log out user
if (response.status === 401) {
Auth.logout();
}
// Handle failed responses
let error;
try {
// Expect API to return JSON
error = JSON.parse(response);
} catch (e) {
// Expect call to fail without JSON response
error = { error: response.statusText };
}
error.statusCode = response.status;
error.response = response;
reject(error);
})
.then((response) => {
return response.json();
})
.then((json) => {
resolve(json);
})
.catch(() => {
reject({ error: 'Unknown error' });
});
});
}
post = (path, data) => {
return this.fetch(path, 'POST', data);
}
put = (path, data) => {
return this.fetch(path, 'PUT', data);
}
get = (path, data) => {
return this.fetch(path, 'GET', data);
}
delete = (path, data) => {
return this.fetch(path, 'DELETE', data);
}
// Helpers
constructQueryString = (data) => {
return _.map(data, (v, k) => {
return `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
}).join('&');
};
}
export default ApiClient;
// In case you don't want to always initiate, just import with `import { client } ...`
const client = new ApiClient();
export { client };

42
src/Utils/Auth.js Normal file
View File

@ -0,0 +1,42 @@
// Inspired by https://github.com/reactjs/react-router/blob/master/examples/auth-flow/auth.js
import Constants from '../Constants';
import History from './History';
import { client } from './ApiClient';
export default {
login(email, password) {
return new Promise((resolve, reject) => {
client.post('/authenticate', {
email,
password,
})
.then((data) => {
localStorage.setItem(Constants.JWT_STORE_KEY, data.jwt_token);
this.onChange(true);
resolve(data);
})
.catch((error) => {
reject(error);
});
});
},
getToken() {
return localStorage.getItem(Constants.JWT_STORE_KEY);
},
logout() {
localStorage.removeItem(Constants.JWT_STORE_KEY);
History.push(Constants.LOGIN_PATH);
this.onChange(false);
},
loggedIn() {
return !!localStorage.getItem(Constants.JWT_STORE_KEY);
},
onChange() {
// This is overriden with a callback function in `Views/App/App.js`
},
};

3
src/Utils/History.js Normal file
View File

@ -0,0 +1,3 @@
// https://github.com/reactjs/react-router/blob/master/docs/guides/NavigatingOutsideOfComponents.md
import { browserHistory } from 'react-router';
export default browserHistory;

42
src/Utils/Markdown.js Normal file
View File

@ -0,0 +1,42 @@
import toMd from 'to-markdown';
const liConverter = {
filter: 'li',
replacement: (content, node) => {
// Change `replace(/\n/gm, '\n ')` to work with our case here :/
content = content.replace(/^\s+/, '').replace(/\n/gm, '\n ');
var prefix = '- ';
var parent = node.parentNode;
var index = Array.prototype.indexOf.call(parent.children, node) + 1;
prefix = /ol/i.test(parent.nodeName) ? index + '. ' : '- ';
return prefix + content;
}
};
const ulConverter = {
filter: ['ul', 'ol'],
replacement: function (content, node) {
var strings = [];
for (var i = 0; i < node.childNodes.length; i++) {
strings.push(node.childNodes[i]._replacement);
}
if (/li/i.test(node.parentNode.nodeName)) {
return '\n' + strings.join('\n');
}
return '\n\n' + strings.join('\n') + '\n\n';
}
};
export function toMarkdown(html) {
console.log(html);
const markdown = toMd(
html, {
gfm: true,
converters: [ liConverter, ulConverter ],
},
);
console.log(markdown);
return markdown;
}

81
src/Views/App/App.js Normal file
View File

@ -0,0 +1,81 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import 'normalize.css/normalize.css';
import styles from './App.scss';
import { toggleEditors } from '../../Actions';
import Header from '../../Components/Header';
import Auth from '../../Utils/Auth';
class App extends Component {
static propTypes = {
children: React.PropTypes.element,
activeEditors: React.PropTypes.isRequired,
toggleEditors: React.PropTypes.func.isRequired,
}
static defaultProps = {}
state = {
loggedIn: Auth.loggedIn(),
}
componentWillMount = () => {
Auth.onChange = this.updateAuth;
}
updateAuth = (loggedIn) => {
this.setState({
loggedIn,
});
}
logout = () => {
// TODO: Replace with Redux actions
Auth.logout();
}
render() {
return (
<div className={ styles.container }>
<Header
activeEditors={this.props.activeEditors}
toggleEditors={this.props.toggleEditors}
/>
<div className={ styles.content }>
{ this.props.children }
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
activeEditors: state.activeEditors,
};
};
const mapDispatchToProps = (dispatch) => {
return {
toggleEditors: (toggledEditor) => {
dispatch(toggleEditors(toggledEditor));
},
};
};
App = connect(
mapStateToProps,
mapDispatchToProps,
)(App);
export default App;
// {this.state.loggedIn ? (
// <a href="#" onClick={this.logout}>Logout</a>
// ) : (
// <Link to="/login">Login</Link>
// )}

13
src/Views/App/App.scss Normal file
View File

@ -0,0 +1,13 @@
.container {
display: flex;
flex-flow: column;
width: 100%;
height: 100%;
background-color: #fff;
font-family: -apple-system, "Helvetica Neue", "Lucida Grande";
color: #222;
}
.content {
}

2
src/Views/App/index.js Normal file
View File

@ -0,0 +1,2 @@
import App from './App';
export default App;

View File

@ -0,0 +1,80 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import MarkdownEditor from '../../Components/MarkdownEditor';
import TextEditor from '../../Components/TextEditor';
import { toMarkdown } from '../../Utils/Markdown';
import { updateText } from '../../Actions';
import styles from './Dashboard.scss';
class Dashboard extends Component {
static propTypes = {
editMarkdown: React.PropTypes.func.isRequired,
editText: React.PropTypes.func.isRequired,
text: React.PropTypes.string,
activeEditors: React.PropTypes.array,
}
// componentDidMount = () => {
// client.get('/user')
// .then(data => {
// this.setState({ user: data });
// });
// }
render() {
const activeEditors = this.props.activeEditors;
return (
<div className={ styles.container }>
{
activeEditors.includes('MARKDOWN') ? (
<div className={ `${activeEditors.length > 1 ?
styles.panel : styles.fullscreen} ${styles.markdown}`}
>
<MarkdownEditor onChange={this.props.editMarkdown} text={this.props.text} />
</div>
) : null
}
{
activeEditors.includes('TEXT') ? (
<div className={ `${activeEditors.length > 1 ?
styles.panel : styles.fullscreen} ${styles.text}`}
>
<TextEditor onChange={this.props.editText} text={this.props.text} />
</div>
) : null
}
</div>
);
}
}
const mapStateToProps = (state) => {
return {
text: state.text,
editor: state.editor,
activeEditors: state.activeEditors,
};
};
const mapDispatchToProps = (dispatch) => {
return {
editMarkdown: (text) => {
dispatch(updateText(text, 'markdown'));
},
editText: (html) => {
const text = toMarkdown(html);
dispatch(updateText(text, 'text'));
},
};
};
Dashboard = connect(
mapStateToProps,
mapDispatchToProps,
)(Dashboard);
export default Dashboard;

View File

@ -0,0 +1,15 @@
.container {
display: flex;
}
.panel {
width: 50%;
}
.fullscreen {
width: 100%;
}
.markdown {
background-color: #fbfbfb;
}

View File

@ -0,0 +1,2 @@
import Dashboard from './Dashboard';
export default Dashboard;

75
src/Views/Login/Login.js Normal file
View File

@ -0,0 +1,75 @@
import React, { Component } from 'react';
import Auth from '../../Utils/Auth';
export default class Login extends Component {
static propTypes = {
location: React.PropTypes.object,
}
static contextTypes = {
router: React.PropTypes.object.isRequired,
}
state = {
email: '',
password: '',
error: null,
}
handleEmailChange = (event) => {
this.setState({ email: event.target.value });
}
handlePasswordChange = (event) => {
this.setState({ password: event.target.value });
}
handleSubmit = (event) => {
event.preventDefault();
Auth.login(this.state.email, this.state.password)
.then(() => {
const { location } = this.props;
if (location.state && location.state.nextPathname) {
this.context.router.replace(location.state.nextPathname);
} else {
this.context.router.replace('/dashboard');
}
})
.catch((err) => {
this.setState({ error: err.error });
});
}
render() {
return (
<div>
<h2>Login</h2>
<form action="" onSubmit={ this.handleSubmit }>
{this.state.error && (
<p>{ this.state.error }</p>
)}
<div>
<input
placeholder={ 'Email' }
onChange={ this.handleEmailChange }
/>
</div>
<div>
<input
placeholder={ 'Password' }
type={ 'password' }
onChange={ this.handlePasswordChange }
/>
</div>
<div>
<input type={ 'submit' } />
</div>
</form>
</div>
);
}
}

View File

2
src/Views/Login/index.js Normal file
View File

@ -0,0 +1,2 @@
import Login from './Login';
export default Login;

41
src/index.js Normal file
View File

@ -0,0 +1,41 @@
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { Router, Route } from 'react-router';
import { createStore, compose } from 'redux';
import History from './Utils/History';
import Auth from './Utils/Auth';
import reducers from './Reducers';
import App from './Views/App';
import Login from './Views/Login';
import Dashboard from './Views/Dashboard';
const store = createStore(
reducers,
compose(
window.devToolsExtension ? window.devToolsExtension() : f => f
)
);
function requireAuth(nextState, replace) {
if (!Auth.loggedIn()) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname },
});
}
}
render((
<Provider store={store}>
<Router history={History}>
<Route path="/" component={App}>
<Route path="login" component={Login} />
<Route path="dashboard" component={Dashboard} onEnter={requireAuth} />
</Route>
</Router>
</Provider>
), document.getElementById('root'));

18
webpack.config.dev.js Normal file
View File

@ -0,0 +1,18 @@
var path = require('path');
var webpack = require('webpack');
commonWebpackConfig = require('./webpack.config');
developmentWebpackConfig = Object.assign(commonWebpackConfig, {
cache: true,
devtool: 'eval',
entry: [
'webpack-hot-middleware/client',
'./src/index',
],
});
developmentWebpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
developmentWebpackConfig.plugins.push(new webpack.NoErrorsPlugin());
module.exports = developmentWebpackConfig;

41
webpack.config.js Normal file
View File

@ -0,0 +1,41 @@
var path = require('path');
var webpack = require('webpack');
// Load .env
require('dotenv').config();
var definePlugin = new webpack.DefinePlugin({
__DEV__: JSON.stringify(JSON.parse(process.env.BUILD_DEV || 'true')),
__PRERELEASE__: JSON.stringify(JSON.parse(process.env.BUILD_PRERELEASE || 'false'))
});
module.exports = {
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
include: path.join(__dirname, 'src')
},
{ test: /\.json$/, loader: 'json-loader' },
{ test: /\.scss$/, loader: 'style!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!sass?sourceMap' },
{ test: /\.css$/, loader: 'style!css-loader' },
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' } // inline base64 URLs for <=8k images, direct URLs for the rest
]
},
resolve: {
// you can now require('file') instead of require('file.json')
extensions: ['', '.js', '.json']
},
plugins: [
definePlugin,
new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
}),
]
};

33
webpack.config.prod.js Normal file
View File

@ -0,0 +1,33 @@
var path = require('path');
var webpack = require('webpack');
commonWebpackConfig = require('./webpack.config');
productionWebpackConfig = Object.assign(commonWebpackConfig, {
cache: true,
devtool: 'cheap-module-source-map',
entry: './src/index',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
},
});
productionWebpackConfig.plugins.push(new webpack.optimize.OccurenceOrderPlugin());
productionWebpackConfig.plugins.push(
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
);
productionWebpackConfig.plugins.push(
new webpack.DefinePlugin({
'process.env': {
'NODE_ENV': JSON.stringify('production')
}
})
);
module.exports = productionWebpackConfig;