commit af30485e9f19433e4cfc8d18ba60989f4283a822 Author: Jori Lallo Date: Sat Feb 27 13:53:11 2016 -0800 Initial commit diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..15aa9d00e --- /dev/null +++ b/.babelrc @@ -0,0 +1,8 @@ +{ + "presets": ["react", "es2015", "stage-0"], + "env": { + "development": { + "presets": ["react-hmre"] + } + } +} \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..2c1688432 --- /dev/null +++ b/.eslintrc @@ -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, + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..5f28d58b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/* +dist/* +.env +npm-debug.log +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..329b62cb5 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Beautiful Atlas + +## Ideas + +- Create sharable private URLs for notes +- Settings + - Enable :emoji: autoconvert \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..28ed6cbeb --- /dev/null +++ b/index.html @@ -0,0 +1,22 @@ + + + + Beautiful Atlas + + + + +
+ + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 000000000..cd65a38e8 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server.js b/server.js new file mode 100644 index 000000000..dd1e72859 --- /dev/null +++ b/server.js @@ -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); +}); diff --git a/src/Actions/index.js b/src/Actions/index.js new file mode 100644 index 000000000..8b0634779 --- /dev/null +++ b/src/Actions/index.js @@ -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 }; +} diff --git a/src/Components/Header/Header.js b/src/Components/Header/Header.js new file mode 100644 index 000000000..040aba127 --- /dev/null +++ b/src/Components/Header/Header.js @@ -0,0 +1,24 @@ +import React from 'react'; + +import styles from './Header.scss'; + +const Header = ({ activeEditors, toggleEditors }) => { + return ( +
+
Beautiful Atlas
+
+ Markdown + Text +
+
Versions
+
+ ); +}; + +export default Header; diff --git a/src/Components/Header/Header.scss b/src/Components/Header/Header.scss new file mode 100644 index 000000000..157d54a3f --- /dev/null +++ b/src/Components/Header/Header.scss @@ -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; + } +} \ No newline at end of file diff --git a/src/Components/Header/index.js b/src/Components/Header/index.js new file mode 100644 index 000000000..24421ce68 --- /dev/null +++ b/src/Components/Header/index.js @@ -0,0 +1,2 @@ +import Header from './Header'; +export default Header; diff --git a/src/Components/MarkdownEditor/MarkdownEditor.js b/src/Components/MarkdownEditor/MarkdownEditor.js new file mode 100644 index 000000000..2e69a74ad --- /dev/null +++ b/src/Components/MarkdownEditor/MarkdownEditor.js @@ -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 ( +
+ +
+ ); + } +} + +export default MarkdownAtlas; diff --git a/src/Components/MarkdownEditor/MarkdownEditor.scss b/src/Components/MarkdownEditor/MarkdownEditor.scss new file mode 100644 index 000000000..ded1dd455 --- /dev/null +++ b/src/Components/MarkdownEditor/MarkdownEditor.scss @@ -0,0 +1,4 @@ +.container { + width: 70%; + margin: 48px auto; +} \ No newline at end of file diff --git a/src/Components/MarkdownEditor/codemirror.css b/src/Components/MarkdownEditor/codemirror.css new file mode 100644 index 000000000..339ff1ffb --- /dev/null +++ b/src/Components/MarkdownEditor/codemirror.css @@ -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; } \ No newline at end of file diff --git a/src/Components/MarkdownEditor/index.js b/src/Components/MarkdownEditor/index.js new file mode 100644 index 000000000..b4ab100ed --- /dev/null +++ b/src/Components/MarkdownEditor/index.js @@ -0,0 +1,2 @@ +import MarkdownEditor from './MarkdownEditor'; +export default MarkdownEditor; diff --git a/src/Components/TextEditor/TextEditor.js b/src/Components/TextEditor/TextEditor.js new file mode 100644 index 000000000..96b80d44b --- /dev/null +++ b/src/Components/TextEditor/TextEditor.js @@ -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 ( +
+
+ +
+ ); + } +} + +export default TextEditor; diff --git a/src/Components/TextEditor/TextEditor.scss b/src/Components/TextEditor/TextEditor.scss new file mode 100644 index 000000000..845d035e6 --- /dev/null +++ b/src/Components/TextEditor/TextEditor.scss @@ -0,0 +1,5 @@ +.editor { + outline: none; + margin: 0 0 20px 0; + padding: 0 0 20px 0; +} \ No newline at end of file diff --git a/src/Components/TextEditor/index.js b/src/Components/TextEditor/index.js new file mode 100644 index 000000000..0a79673b9 --- /dev/null +++ b/src/Components/TextEditor/index.js @@ -0,0 +1,2 @@ +import TextEditor from './TextEditor'; +export default TextEditor; diff --git a/src/Constants.js b/src/Constants.js new file mode 100644 index 000000000..4b9e2dbbc --- /dev/null +++ b/src/Constants.js @@ -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); diff --git a/src/Reducers/index.js b/src/Reducers/index.js new file mode 100644 index 000000000..77be62db2 --- /dev/null +++ b/src/Reducers/index.js @@ -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; diff --git a/src/Utils/ApiClient.js b/src/Utils/ApiClient.js new file mode 100644 index 000000000..c32c79f93 --- /dev/null +++ b/src/Utils/ApiClient.js @@ -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 }; diff --git a/src/Utils/Auth.js b/src/Utils/Auth.js new file mode 100644 index 000000000..728535462 --- /dev/null +++ b/src/Utils/Auth.js @@ -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` + }, +}; diff --git a/src/Utils/History.js b/src/Utils/History.js new file mode 100644 index 000000000..76445fa09 --- /dev/null +++ b/src/Utils/History.js @@ -0,0 +1,3 @@ +// https://github.com/reactjs/react-router/blob/master/docs/guides/NavigatingOutsideOfComponents.md +import { browserHistory } from 'react-router'; +export default browserHistory; diff --git a/src/Utils/Markdown.js b/src/Utils/Markdown.js new file mode 100644 index 000000000..c0490d79b --- /dev/null +++ b/src/Utils/Markdown.js @@ -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; +} diff --git a/src/Views/App/App.js b/src/Views/App/App.js new file mode 100644 index 000000000..76a676306 --- /dev/null +++ b/src/Views/App/App.js @@ -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 ( +
+
+
+ { this.props.children } +
+
+ ); + } +} + +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 ? ( +// Logout +// ) : ( +// Login +// )} diff --git a/src/Views/App/App.scss b/src/Views/App/App.scss new file mode 100644 index 000000000..ba81774a1 --- /dev/null +++ b/src/Views/App/App.scss @@ -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 { +} \ No newline at end of file diff --git a/src/Views/App/index.js b/src/Views/App/index.js new file mode 100644 index 000000000..54374b9b6 --- /dev/null +++ b/src/Views/App/index.js @@ -0,0 +1,2 @@ +import App from './App'; +export default App; diff --git a/src/Views/Dashboard/Dashboard.js b/src/Views/Dashboard/Dashboard.js new file mode 100644 index 000000000..0f92efc88 --- /dev/null +++ b/src/Views/Dashboard/Dashboard.js @@ -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 ( +
+ { + activeEditors.includes('MARKDOWN') ? ( +
1 ? + styles.panel : styles.fullscreen} ${styles.markdown}`} + > + +
+ ) : null + } + { + activeEditors.includes('TEXT') ? ( +
1 ? + styles.panel : styles.fullscreen} ${styles.text}`} + > + +
+ ) : null + } +
+ ); + } +} + +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; diff --git a/src/Views/Dashboard/Dashboard.scss b/src/Views/Dashboard/Dashboard.scss new file mode 100644 index 000000000..80d56502a --- /dev/null +++ b/src/Views/Dashboard/Dashboard.scss @@ -0,0 +1,15 @@ +.container { + display: flex; +} + +.panel { + width: 50%; +} + +.fullscreen { + width: 100%; +} + +.markdown { + background-color: #fbfbfb; +} \ No newline at end of file diff --git a/src/Views/Dashboard/index.js b/src/Views/Dashboard/index.js new file mode 100644 index 000000000..e5cb98c3e --- /dev/null +++ b/src/Views/Dashboard/index.js @@ -0,0 +1,2 @@ +import Dashboard from './Dashboard'; +export default Dashboard; diff --git a/src/Views/Login/Login.js b/src/Views/Login/Login.js new file mode 100644 index 000000000..00191dfa9 --- /dev/null +++ b/src/Views/Login/Login.js @@ -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 ( +
+

Login

+
+ {this.state.error && ( +

{ this.state.error }

+ )} + +
+ +
+
+ +
+
+ +
+
+
+ ); + } +} diff --git a/src/Views/Login/Login.scss b/src/Views/Login/Login.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/Views/Login/index.js b/src/Views/Login/index.js new file mode 100644 index 000000000..189be5f01 --- /dev/null +++ b/src/Views/Login/index.js @@ -0,0 +1,2 @@ +import Login from './Login'; +export default Login; diff --git a/src/index.js b/src/index.js new file mode 100644 index 000000000..1d248aadc --- /dev/null +++ b/src/index.js @@ -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(( + + + + + + + + +), document.getElementById('root')); diff --git a/webpack.config.dev.js b/webpack.config.dev.js new file mode 100644 index 000000000..d2656ab7c --- /dev/null +++ b/webpack.config.dev.js @@ -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; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..b132bcdaa --- /dev/null +++ b/webpack.config.js @@ -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' + }), + ] +}; \ No newline at end of file diff --git a/webpack.config.prod.js b/webpack.config.prod.js new file mode 100644 index 000000000..16d9efb4a --- /dev/null +++ b/webpack.config.prod.js @@ -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;