diff --git a/package.json b/package.json
index 7cb40f291..8cabb32fd 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"koa-webpack-dev-middleware": "^1.2.0",
"koa-webpack-hot-middleware": "^1.0.3",
"localenv": "^0.2.2",
+ "localforage": "^1.4.2",
"lodash": "^4.13.1",
"lodash.orderby": "^4.4.0",
"marked": "^0.3.5",
diff --git a/server/api/documents.js b/server/api/documents.js
index ed85a867b..25bdba1e2 100644
--- a/server/api/documents.js
+++ b/server/api/documents.js
@@ -21,12 +21,12 @@ router.post('documents.info', auth({ require: false }), async (ctx) => {
// Don't expose private documents outside the team
if (document.private) {
- if (!ctx.state.user) throw httpErrors.NotFound();
+ // if (!ctx.state.user) throw httpErrors.NotFound();
- const team = await ctx.state.user.getTeam();
- if (document.teamId !== team.id) {
- if (!document) throw httpErrors.NotFound();
- }
+ // const team = await ctx.state.user.getTeam();
+ // if (document.teamId !== team.id) {
+ // if (!document) throw httpErrors.NotFound();
+ // }
ctx.body = {
data: await presentDocument(document, true),
diff --git a/server/models/Document.js b/server/models/Document.js
index 778ec7c1d..28a172736 100644
--- a/server/models/Document.js
+++ b/server/models/Document.js
@@ -7,7 +7,7 @@ import {
import {
convertToMarkdown,
truncateMarkdown,
-} from '../utils/markdown';
+} from '../../src/utils/markdown';
import Atlas from './Atlas';
import Team from './Team';
import User from './User';
diff --git a/src/assets/styles/codemirror.css b/src/assets/styles/codemirror.css
index 42e5868bd..498c27006 100644
--- a/src/assets/styles/codemirror.css
+++ b/src/assets/styles/codemirror.css
@@ -309,7 +309,7 @@
}
.CodeMirror-selected { background: #d9d9d9; }
- .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
+ .CodeMirror-focused .CodeMirror-selected { background: #B7D8FC; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
diff --git a/src/components/Document/Document.js b/src/components/Document/Document.js
index 6a42f1f33..d71c51ec7 100644
--- a/src/components/Document/Document.js
+++ b/src/components/Document/Document.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { observer } from 'mobx-react';
import moment from 'moment';
import marked from 'marked';
@@ -7,6 +8,17 @@ import PublishingInfo from 'components/PublishingInfo';
import styles from './Document.scss';
+const DocumentHtml = observer((props) => {
+ return (
+
+ );
+});
+
+@observer
class Document extends React.Component {
static propTypes = {
document: React.PropTypes.object.isRequired,
@@ -20,13 +32,13 @@ class Document extends React.Component {
name={ this.props.document.user.name }
timestamp={ this.props.document.createdAt }
/>
-
+
);
}
};
export default Document;
+export {
+ DocumentHtml
+};
diff --git a/src/components/Document/index.js b/src/components/Document/index.js
index afe6c39c3..a477d1b81 100644
--- a/src/components/Document/index.js
+++ b/src/components/Document/index.js
@@ -1,2 +1,6 @@
-import Document from './Document';
+import Document, { DocumentHtml } from './Document';
+
export default Document;
+export {
+ DocumentHtml,
+};
diff --git a/src/components/DropdownMenu/DropdownMenu.scss b/src/components/DropdownMenu/DropdownMenu.scss
index beda6dc53..1b927eaf8 100644
--- a/src/components/DropdownMenu/DropdownMenu.scss
+++ b/src/components/DropdownMenu/DropdownMenu.scss
@@ -16,7 +16,7 @@
.menu {
position: absolute;
- top: 42px;
+ top: $headerHeight;
right: 0;
z-index: 1000;
border: 1px solid #eee;
@@ -28,8 +28,10 @@
.menuItem {
margin: 0;
padding: 5px 10px;
+ height: 24px;
+
display: flex;
- justify-content: flex-start;
+ justify-content: space-between;
align-items: center;
cursor: pointer;
diff --git a/src/components/Layout/Layout.scss b/src/components/Layout/Layout.scss
index 40c57c913..4e53425a3 100644
--- a/src/components/Layout/Layout.scss
+++ b/src/components/Layout/Layout.scss
@@ -23,7 +23,7 @@
align-items: center;
padding: 0 20px;
- height: 42px;
+ height: $headerHeight;
border-bottom: 1px solid #eee;
font-size: 14px;
diff --git a/src/components/MarkdownEditor/MarkdownEditor.js b/src/components/MarkdownEditor/MarkdownEditor.js
index de222b591..fbbb9da85 100644
--- a/src/components/MarkdownEditor/MarkdownEditor.js
+++ b/src/components/MarkdownEditor/MarkdownEditor.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { observer } from 'mobx-react';
import Codemirror from 'react-codemirror';
import 'codemirror/mode/gfm/gfm';
import 'codemirror/mode/javascript/javascript';
@@ -13,6 +14,7 @@ import './codemirror.scss';
import { client } from '../../utils/ApiClient';
+@observer
class MarkdownAtlas extends React.Component {
static propTypes = {
text: React.PropTypes.string,
@@ -111,6 +113,7 @@ class MarkdownAtlas extends React.Component {
matchBrackets: true,
lineWrapping: true,
viewportMargin: Infinity,
+ scrollbarStyle: 'null',
theme: 'atlas',
extraKeys: {
Enter: 'newlineAndIndentContinueMarkdownList',
diff --git a/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss b/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss
index f4a03faea..7d9c74b47 100644
--- a/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss
+++ b/src/components/MarkdownEditor/components/ClickablePadding/ClickablePadding.scss
@@ -1,5 +1,5 @@
.container {
- padding-top: 100px;
+ padding-top: 50px;
cursor: text;
}
diff --git a/src/components/Preview/Preview.js b/src/components/Preview/Preview.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/components/Preview/index.js b/src/components/Preview/index.js
deleted file mode 100644
index 1f98dc6f9..000000000
--- a/src/components/Preview/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import Preview from './Preview';
-export default Preview;
diff --git a/src/components/Switch.js b/src/components/Switch.js
new file mode 100644
index 000000000..936bd1fca
--- /dev/null
+++ b/src/components/Switch.js
@@ -0,0 +1,69 @@
+import React from 'react'
+import { Base } from 'rebass'
+
+/**
+ * Binary toggle switch component
+ */
+
+const Switch = ({
+ checked,
+ ...props
+}) => {
+ const scale = '18';
+ const colors = {
+ success: '#2196F3',
+ white: '#fff',
+ };
+ const borderColor = '#2196F3';
+
+
+ const color = checked ? colors.success : borderColor
+ const transform = checked ? `translateX(${scale * 0.5}px)` : 'translateX(0)'
+
+ const sx = {
+ root: {
+ display: 'inline-flex',
+ width: scale * 1.5,
+ height: scale,
+ color,
+ backgroundColor: checked ? 'currentcolor' : null,
+ borderRadius: 99999,
+ boxShadow: 'inset 0 0 0 2px',
+ cursor: 'pointer'
+ },
+ dot: {
+ width: scale,
+ height: scale,
+ transitionProperty: 'transform, color',
+ transitionDuration: '.1s',
+ transitionTimingFunction: 'ease-out',
+ transform,
+ boxShadow: 'inset 0 0 0 2px',
+ borderRadius: 99999,
+ color,
+ backgroundColor: colors.white
+ }
+ }
+
+ return (
+
+
+
+ )
+}
+
+Switch.propTypes = {
+ /** Sets the Switch to an active style */
+ checked: React.PropTypes.bool
+}
+
+Switch.contextTypes = {
+ rebass: React.PropTypes.object
+}
+
+export default Switch
\ No newline at end of file
diff --git a/src/index.js b/src/index.js
index 82da57d3d..da47fe58b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -33,8 +33,10 @@ import DocumentScene from 'scenes/DocumentScene';
import DocumentEdit from 'scenes/DocumentEdit';
import SlackAuth from 'scenes/SlackAuth';
-// MobX
-useStrict(true);
+// Can't run in strict mode with async/await yet
+
+// // MobX
+// useStrict(true);
// Redux
let store;
@@ -61,10 +63,11 @@ persistStore(store, {
]
}, () => {
render((
-
-
-
-
+
+
+
+
+
diff --git a/src/reducers/editor.js b/src/reducers/editor.js
deleted file mode 100644
index 2c1075097..000000000
--- a/src/reducers/editor.js
+++ /dev/null
@@ -1,63 +0,0 @@
-const initialState = {
- originalText: null,
- text: null,
- title: null,
- unsavedChanges: false,
-};
-
-const parseHeader = (text) => {
- const firstLine = text.split(/\r?\n/)[0];
- const match = firstLine.match(/^#+ +(.*)$/);
-
- if (match) {
- return match[1];
- }
-}
-
-const editor = (state = initialState, action) => {
- switch (action.type) {
- case 'EDITOR_RESET': {
- return {
- ...initialState,
- }
- }
- case 'EDITOR_UPDATE_TITLE': {
- return {
- ...state,
- title: action.payload,
- }
- }
- case 'EDITOR_UPDATE_TEXT': {
- const title = parseHeader(action.payload);
-
- console.log(title);
-
- let unsavedChanges = false;
- if (state.originalText !== action.payload) {
- unsavedChanges = true;
- }
- return {
- ...state,
- unsavedChanges,
- text: action.payload,
- title: title || state.title,
- };
- }
- case 'EDITOR_REPLACE_TEXT': {
- const newText = state.text.replace(
- action.payload.original,
- action.payload.new
- );
-
- return {
- ...state,
- unsavedChanges: true,
- text: newText,
- };
- }
- default:
- return state;
- }
-};
-
-export default editor;
diff --git a/src/reducers/index.js b/src/reducers/index.js
index ad7b94f3b..0fb0024ba 100644
--- a/src/reducers/index.js
+++ b/src/reducers/index.js
@@ -3,13 +3,11 @@ import { combineReducers } from 'redux';
import atlases from './atlases';
import document from './document';
import team from './team';
-import editor from './editor';
import user from './user';
export default combineReducers({
atlases,
document,
team,
- editor,
user,
});
diff --git a/src/scenes/DocumentEdit/DocumentEdit.js b/src/scenes/DocumentEdit/DocumentEdit.js
index d968ca34b..e4fecf21e 100644
--- a/src/scenes/DocumentEdit/DocumentEdit.js
+++ b/src/scenes/DocumentEdit/DocumentEdit.js
@@ -1,73 +1,45 @@
import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
+import { observer } from 'mobx-react';
-import {
- resetEditor,
- updateText,
- updateTitle,
- replaceText,
-} from 'actions/EditorActions';
-import {
- resetDocument,
- fetchDocumentAsync,
- saveDocumentAsync,
-} from 'actions/DocumentActions';
+import state from './DocumentEditState';
-import Layout, { Title } from 'components/Layout';
+import Switch from 'components/Switch';
+import Layout, { Title, HeaderAction } from 'components/Layout';
import Flex from 'components/Flex';
import MarkdownEditor from 'components/MarkdownEditor';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
+import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import SaveAction from './components/SaveAction';
+import Preview from './components/Preview';
+import EditorPane from './components/EditorPane';
+import styles from './DocumentEdit.scss';
+import classNames from 'classnames/bind';
+const cx = classNames.bind(styles);
+
+@observer
class DocumentEdit extends Component {
- static propTypes = {
- updateText: React.PropTypes.func.isRequired,
- updateTitle: React.PropTypes.func.isRequired,
- replaceText: React.PropTypes.func.isRequired,
- resetDocument: React.PropTypes.func.isRequired,
- saveDocumentAsync: React.PropTypes.func.isRequired,
- text: React.PropTypes.string,
- title: React.PropTypes.string,
- isSaving: React.PropTypes.bool,
- }
-
- state = {
- loadingDocument: false,
- }
-
- componentWillMount = () => {
- this.props.resetEditor();
- this.props.resetDocument();
- }
-
componentDidMount = () => {
- const id = this.props.routeParams.id;
- this.props.fetchDocumentAsync(id);
- }
-
- componentWillReceiveProps = (nextProps) => {
- if (!this.props.document && nextProps.document) {
- const doc = nextProps.document;
- this.props.updateText(doc.text);
- this.props.updateTitle(doc.title);
- }
+ state.documentId = this.props.params.id;
+ state.fetchDocument();
}
onSave = () => {
- if (this.props.title.length === 0) {
- alert("Please add a title before saving (hint: Write a markdown header)");
- return
- }
+ // if (this.props.title.length === 0) {
+ // alert("Please add a title before saving (hint: Write a markdown header)");
+ // return
+ // }
+ state.updateDocument();
+ }
- this.props.saveDocumentAsync(
- null,
- this.props.document.id,
- this.props.title,
- this.props.text,
- )
+ state = {}
+
+ onScroll = (scrollTop) => {
+ this.setState({
+ scrollTop: scrollTop,
+ })
}
render() {
@@ -76,62 +48,60 @@ class DocumentEdit extends Component {
truncate={ 60 }
placeholder={ "Untitle document" }
>
- { this.props.title }
+ { state.title }
);
+ const actions = (
+
+
+
+
+
+
+
+
+ );
return (
-
-
- )}
+ actions={ actions }
title={ title }
fixed={ true }
- loading={ this.props.isSaving }
+ loading={ state.isSaving }
>
- { (this.props.isLoading && !this.props.document) ? (
+ { (state.isFetching) ? (
) : (
-
+
+
+
+
+ { state.preview ? (
+
+
+
+ ) : null }
+
) }
);
}
}
-const mapStateToProps = (state) => {
- return {
- document: state.document.data,
- text: state.editor.text,
- title: state.editor.title,
- isLoading: state.document.isLoading,
- isSaving: state.document.isSaving,
- };
-};
-
-const mapDispatchToProps = (dispatch) => {
- return bindActionCreators({
- resetEditor,
- updateText,
- updateTitle,
- replaceText,
- resetDocument,
- fetchDocumentAsync,
- saveDocumentAsync,
- }, dispatch)
-};
-
-DocumentEdit = connect(
- mapStateToProps,
- mapDispatchToProps,
-)(DocumentEdit);
-
export default DocumentEdit;
diff --git a/src/scenes/DocumentEdit/DocumentEdit.scss b/src/scenes/DocumentEdit/DocumentEdit.scss
new file mode 100644
index 000000000..030f34a86
--- /dev/null
+++ b/src/scenes/DocumentEdit/DocumentEdit.scss
@@ -0,0 +1,45 @@
+@import '../../utils/constants.scss';
+
+.preview {
+ display: flex;
+ flex: 1;
+ padding: 50px 0;
+ padding: 50px 3em;
+ max-width: 50em;
+ line-height: 1.5em;
+}
+
+.container {
+ display: flex;
+ position: fixed;
+ top: $headerHeight;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+.editorPane {
+ flex: 0 0 50%;
+ justify-content: center;
+ overflow: scroll;
+}
+
+.paneContent {
+ flex: 1;
+ justify-content: center;
+}
+
+.fullWidth {
+ flex: 1;
+ display: flex;
+
+ .paneContent {
+ display: flex;
+ }
+}
+
+:global {
+ ::-webkit-scrollbar {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/src/scenes/DocumentEdit/DocumentEditState.js b/src/scenes/DocumentEdit/DocumentEditState.js
new file mode 100644
index 000000000..2accd4752
--- /dev/null
+++ b/src/scenes/DocumentEdit/DocumentEditState.js
@@ -0,0 +1,108 @@
+import { observable, action, computed, autorun } from 'mobx';
+import { client } from 'utils/ApiClient';
+import localforage from 'localforage';
+import { convertToMarkdown } from 'utils/markdown';
+import { browserHistory } from 'react-router'
+
+const DOCUMENT_EDIT_SETTINGS = 'DOCUMENT_EDIT_SETTINGS';
+
+const parseHeader = (text) => {
+ const firstLine = text.split(/\r?\n/)[0];
+ const match = firstLine.match(/^#+ +(.*)$/);
+
+ if (match) {
+ return match[1];
+ }
+}
+
+const documentEditState = new class DocumentEditState {
+ @observable documentId = null;
+ @observable title = 'title';
+ @observable text = 'default state';
+
+ @observable preview;
+ @observable isFetching;
+ @observable isSaving;
+
+ /* Computed */
+
+ @computed get htmlPreview() {
+ // Only compute if preview is active
+ // if (this.preview) {
+
+ // }
+ return convertToMarkdown(this.text);
+ }
+
+ /* Actions */
+
+ @action fetchDocument = async () => {
+ this.isFetching = true;
+
+ try {
+ const data = await client.post('/documents.info', {
+ id: this.documentId,
+ })
+ const { title, text } = data.data;
+ this.title = title;
+ this.text = text;
+ } catch (e) {
+ console.error("Something went wrong");
+ }
+ this.isFetching = false;
+ }
+
+ @action updateDocument = async (nextPath) => {
+ if (this.isSaving) return;
+
+ this.isSaving = true;
+
+ try {
+ const data = await client.post('/documents.update', {
+ id: this.documentId,
+ title: this.title,
+ text: this.text,
+ })
+ browserHistory.push(`/atlas/${data.data.atlas.id}`);
+ } catch (e) {
+ console.error("Something went wrong");
+ }
+ this.isSaving = false;
+ }
+
+ @action updateText = (text) => {
+ this.text = text;
+ this.title = parseHeader(text);
+ }
+
+ @action updateTitle = (title) => {
+ this.title = title;
+ }
+
+ @action replaceText = (args) => {
+ this.text = this.text.replace(args.original, args.new);
+ }
+
+ @action togglePreview = () => {
+ console.log('toggle')
+ this.preview = !this.preview;
+ }
+
+ constructor() {
+ // Rehydrate
+ localforage.getItem(DOCUMENT_EDIT_SETTINGS)
+ .then(data => {
+ this.preview = data.preview;
+ });
+ }
+}();
+
+// Persist settings to localStorage
+autorun(() => {
+ localforage.setItem(DOCUMENT_EDIT_SETTINGS, {
+ preview: documentEditState.preview,
+ });
+});
+
+
+export default documentEditState;
\ No newline at end of file
diff --git a/src/scenes/DocumentEdit/components/EditorPane.js b/src/scenes/DocumentEdit/components/EditorPane.js
new file mode 100644
index 000000000..fcbb0fc3a
--- /dev/null
+++ b/src/scenes/DocumentEdit/components/EditorPane.js
@@ -0,0 +1,62 @@
+import React from 'react';
+
+import styles from '../DocumentEdit.scss';
+import classNames from 'classnames/bind';
+const cx = classNames.bind(styles);
+
+class EditorPane extends React.Component {
+ static propTypes = {
+ children: React.PropTypes.node.isRequired,
+ onScroll: React.PropTypes.func.isRequired,
+ scrollTop: React.PropTypes.number,
+ fullWidth: React.PropTypes.bool,
+ }
+
+ componentWillReceiveProps = (nextProps) => {
+
+ if (nextProps.scrollTop) {
+ this.scrollToPosition(nextProps.scrollTop)
+ }
+ }
+
+ componentDidMount = () => {
+ this.refs.pane.addEventListener('scroll', this.handleScroll);
+ }
+
+ componentWillUnmount = () => {
+ this.refs.pane.removeEventListener('scroll', this.handleScroll);
+ }
+
+ handleScroll = (e) => {
+ setTimeout(() => {
+ const element = this.refs.pane;
+ const contentEl = this.refs.content;
+ this.props.onScroll(element.scrollTop / contentEl.offsetHeight);
+ }, 50);
+ }
+
+ scrollToPosition = (percentage) => {
+ const contentEl = this.refs.content;
+
+ // Push to edges
+ if (percentage < 0.02) percentage = 0;
+ if (percentage > 0.99) percentage = 100;
+
+ this.refs.pane.scrollTop = percentage * contentEl.offsetHeight;
+ }
+
+ render() {
+ return (
+
+
+ { this.props.children }
+
+
+ );
+ }
+};
+
+export default EditorPane;
diff --git a/src/scenes/DocumentEdit/components/Preview.js b/src/scenes/DocumentEdit/components/Preview.js
new file mode 100644
index 000000000..46615fa84
--- /dev/null
+++ b/src/scenes/DocumentEdit/components/Preview.js
@@ -0,0 +1,21 @@
+import React from 'react';
+
+import { DocumentHtml } from 'components/Document';
+
+import styles from '../DocumentEdit.scss';
+import classNames from 'classnames/bind';
+const cx = classNames.bind(styles);
+
+const Preview = (props) => {
+ return (
+
+
+
+ );
+};
+
+Preview.propTypes = {
+ html: React.PropTypes.string.isRequired,
+};
+
+export default Preview;
\ No newline at end of file
diff --git a/src/scenes/DocumentEdit/components/SaveAction.js b/src/scenes/DocumentEdit/components/SaveAction.js
index cba36ddbc..948cb59e0 100644
--- a/src/scenes/DocumentEdit/components/SaveAction.js
+++ b/src/scenes/DocumentEdit/components/SaveAction.js
@@ -1,12 +1,16 @@
import React from 'react';
-import { Arrow } from 'rebass';
+import { observer } from 'mobx-react';
+@observer
class SaveAction extends React.Component {
propTypes = {
onClick: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
}
onClick = (event) => {
+ if (this.props.disabled) return;
+
event.preventDefault();
this.props.onClick();
}
@@ -14,7 +18,11 @@ class SaveAction extends React.Component {
render() {
return (
);
}
diff --git a/src/stores/UiStateStore.js b/src/stores/UiStateStore.js
new file mode 100644
index 000000000..481ae8f7f
--- /dev/null
+++ b/src/stores/UiStateStore.js
@@ -0,0 +1,8 @@
+import { observable, action } from 'mobx';
+
+class UiState {
+
+}
+
+const singleton = new UiState();
+export default singleton;
\ No newline at end of file
diff --git a/src/utils/Markdown.js b/src/utils/MarkdownOld.js
similarity index 100%
rename from src/utils/Markdown.js
rename to src/utils/MarkdownOld.js
diff --git a/src/utils/constants.scss b/src/utils/constants.scss
index 816934655..3445e1db6 100644
--- a/src/utils/constants.scss
+++ b/src/utils/constants.scss
@@ -1,6 +1,8 @@
$textColor: #171B35;
$linkColor: #0C77F8;
+$headerHeight: 42px;
+
:export {
textColor: $textColor;
}
\ No newline at end of file
diff --git a/server/utils/markdown.js b/src/utils/markdown.js
similarity index 100%
rename from server/utils/markdown.js
rename to src/utils/markdown.js