Initial commit
This commit is contained in:
commit
af30485e9f
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"presets": ["react", "es2015", "stage-0"],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
node_modules/*
|
||||
dist/*
|
||||
.env
|
||||
npm-debug.log
|
||||
.DS_Store
|
|
@ -0,0 +1,7 @@
|
|||
# Beautiful Atlas
|
||||
|
||||
## Ideas
|
||||
|
||||
- Create sharable private URLs for notes
|
||||
- Settings
|
||||
- Enable :emoji: autoconvert
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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 };
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import Header from './Header';
|
||||
export default Header;
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
|||
.container {
|
||||
width: 70%;
|
||||
margin: 48px auto;
|
||||
}
|
|
@ -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; }
|
|
@ -0,0 +1,2 @@
|
|||
import MarkdownEditor from './MarkdownEditor';
|
||||
export default MarkdownEditor;
|
|
@ -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;
|
|
@ -0,0 +1,5 @@
|
|||
.editor {
|
||||
outline: none;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0 0 20px 0;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import TextEditor from './TextEditor';
|
||||
export default TextEditor;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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`
|
||||
},
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
// https://github.com/reactjs/react-router/blob/master/docs/guides/NavigatingOutsideOfComponents.md
|
||||
import { browserHistory } from 'react-router';
|
||||
export default browserHistory;
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
// )}
|
|
@ -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 {
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import App from './App';
|
||||
export default App;
|
|
@ -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;
|
|
@ -0,0 +1,15 @@
|
|||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
background-color: #fbfbfb;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import Dashboard from './Dashboard';
|
||||
export default Dashboard;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import Login from './Login';
|
||||
export default Login;
|
|
@ -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'));
|
|
@ -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;
|
|
@ -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'
|
||||
}),
|
||||
]
|
||||
};
|
|
@ -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;
|
Reference in New Issue