Initial commit
This commit is contained in:
29
src/Actions/index.js
Normal file
29
src/Actions/index.js
Normal 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 };
|
||||
}
|
||||
24
src/Components/Header/Header.js
Normal file
24
src/Components/Header/Header.js
Normal 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;
|
||||
47
src/Components/Header/Header.scss
Normal file
47
src/Components/Header/Header.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
2
src/Components/Header/index.js
Normal file
2
src/Components/Header/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Header from './Header';
|
||||
export default Header;
|
||||
52
src/Components/MarkdownEditor/MarkdownEditor.js
Normal file
52
src/Components/MarkdownEditor/MarkdownEditor.js
Normal 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;
|
||||
4
src/Components/MarkdownEditor/MarkdownEditor.scss
Normal file
4
src/Components/MarkdownEditor/MarkdownEditor.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
.container {
|
||||
width: 70%;
|
||||
margin: 48px auto;
|
||||
}
|
||||
58
src/Components/MarkdownEditor/codemirror.css
Normal file
58
src/Components/MarkdownEditor/codemirror.css
Normal 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; }
|
||||
2
src/Components/MarkdownEditor/index.js
Normal file
2
src/Components/MarkdownEditor/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import MarkdownEditor from './MarkdownEditor';
|
||||
export default MarkdownEditor;
|
||||
52
src/Components/TextEditor/TextEditor.js
Normal file
52
src/Components/TextEditor/TextEditor.js
Normal 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;
|
||||
5
src/Components/TextEditor/TextEditor.scss
Normal file
5
src/Components/TextEditor/TextEditor.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
.editor {
|
||||
outline: none;
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0 0 20px 0;
|
||||
}
|
||||
2
src/Components/TextEditor/index.js
Normal file
2
src/Components/TextEditor/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import TextEditor from './TextEditor';
|
||||
export default TextEditor;
|
||||
19
src/Constants.js
Normal file
19
src/Constants.js
Normal 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
39
src/Reducers/index.js
Normal 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
109
src/Utils/ApiClient.js
Normal 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
42
src/Utils/Auth.js
Normal 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
3
src/Utils/History.js
Normal 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
42
src/Utils/Markdown.js
Normal 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
81
src/Views/App/App.js
Normal 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
13
src/Views/App/App.scss
Normal 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
2
src/Views/App/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import App from './App';
|
||||
export default App;
|
||||
80
src/Views/Dashboard/Dashboard.js
Normal file
80
src/Views/Dashboard/Dashboard.js
Normal 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;
|
||||
15
src/Views/Dashboard/Dashboard.scss
Normal file
15
src/Views/Dashboard/Dashboard.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
2
src/Views/Dashboard/index.js
Normal file
2
src/Views/Dashboard/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Dashboard from './Dashboard';
|
||||
export default Dashboard;
|
||||
75
src/Views/Login/Login.js
Normal file
75
src/Views/Login/Login.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
0
src/Views/Login/Login.scss
Normal file
0
src/Views/Login/Login.scss
Normal file
2
src/Views/Login/index.js
Normal file
2
src/Views/Login/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Login from './Login';
|
||||
export default Login;
|
||||
41
src/index.js
Normal file
41
src/index.js
Normal 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'));
|
||||
Reference in New Issue
Block a user