Renamed /src to /frontend

This commit is contained in:
Jori Lallo
2016-07-24 15:32:31 -07:00
parent 19da05eee7
commit d2187c4b10
147 changed files with 10 additions and 10 deletions

View File

@@ -0,0 +1,23 @@
import React from "react";
import { observer } from 'mobx-react';
import Helmet from "react-helmet";
const Application = observer((props) => {
return (
<div style={{ width: '100%', display: 'flex', flex: 1, }}>
<Helmet
title="Beautiful Atlas"
meta={[
{"name": "viewport", "content": "width=device-width, initial-scale=1.0"},
]}
/>
{ props.children }
</div>
);
});
Application.propTypes = {
children: React.PropTypes.node.isRequired,
}
export default Application;

View File

@@ -0,0 +1,96 @@
import React from 'react';
import { observer } from 'mobx-react';
import { Link, browserHistory } from 'react-router';
import keydown, { keydownScoped } from 'react-keydown';
import _ from 'lodash';
import store from './AtlasStore';
import Layout, { Title, HeaderAction } from 'components/Layout';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import DocumentList from 'components/DocumentList';
import Divider from 'components/Divider';
import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
import Flex from 'components/Flex';
import styles from './Atlas.scss';
@keydown(['c'])
@observer
class Atlas extends React.Component {
componentDidMount = () => {
const { id } = this.props.params;
store.fetchAtlas(id, data => {
// Forward directly to root document
if (data.type === 'atlas') {
browserHistory.replace(data.navigationTree.url);
}
})
}
componentWillReceiveProps = (nextProps) => {
const key = nextProps.keydown.event;
if (key) {
if (key.key === 'c') {
_.defer(this.onCreate);
}
}
}
onCreate = (event) => {
if (event) event.preventDefault();
browserHistory.push(`/atlas/${store.atlas.id}/new`);
}
render() {
const atlas = store.atlas;
let actions;
let title;
let titleText;
if (atlas) {
actions = (
<Flex direction="row">
<DropdownMenu label={ <MoreIcon /> } >
<MenuItem onClick={ this.onCreate }>
New document
</MenuItem>
</DropdownMenu>
</Flex>
);
title = <Title>{ atlas.name }</Title>;
titleText = atlas.name;
}
return (
<Layout
actions={ actions }
title={ title }
titleText={ titleText }
>
<CenteredContent>
{ store.isFetching ? (
<AtlasPreviewLoading />
) : (
<div className={ styles.container }>
<div className={ styles.atlasDetails }>
<h2>{ atlas.name }</h2>
<blockquote>
{ atlas.description }
</blockquote>
</div>
<Divider />
<DocumentList documents={ atlas.recentDocuments } preview={ true } />
</div>
) }
</CenteredContent>
</Layout>
);
}
}
export default Atlas;

View File

@@ -0,0 +1,17 @@
.container {
display: flex;
flex: 1;
flex-direction: column;
}
.atlasDetails {
display: flex;
flex: 1;
flex-direction: column;
blockquote {
padding: 0;
margin: 0 0 20px 0;
font-style: italic;
}
}

View File

@@ -0,0 +1,27 @@
import { observable, action } from 'mobx';
import { client } from 'utils/ApiClient';
const store = new class AtlasStore {
@observable atlas;
@observable isFetching = true;
/* Actions */
@action fetchAtlas = async (id, successCallback) => {
this.isFetching = true;
this.atlas = null;
try {
const res = await client.get('/atlases.info', { id: id });
const { data } = res;
this.atlas = data;
successCallback(data);
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
}();
export default store;

View File

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

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { observer } from 'mobx-react';
import store from './DashboardStore';
import Flex from 'components/Flex';
import Layout from 'components/Layout';
import AtlasPreview from 'components/AtlasPreview';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
import FullscreenField from 'components/FullscreenField';
import styles from './Dashboard.scss';
@observer(['user'])
class Dashboard extends React.Component {
static propTypes = {
user: React.PropTypes.object.isRequired,
}
componentDidMount = () => {
store.fetchAtlases(this.props.user.team.id);
}
state = {
newAtlasVisible: false
}
onClickNewAtlas = () => {
this.setState({
newAtlasVisible: true,
});
}
render() {
const actions = (
<Flex direction="row">
<DropdownMenu label={ <MoreIcon /> } >
<MenuItem onClick={ this.onClickNewAtlas }>
New Atlas
</MenuItem>
</DropdownMenu>
</Flex>
);
return (
<Flex flex={ true }>
<Layout
actions={ actions }
>
<CenteredContent>
<Flex direction="column" flex={ true }>
{ store.isFetching ? (
<AtlasPreviewLoading />
) : store.atlases && store.atlases.map((atlas) => {
return (<AtlasPreview key={ atlas.id } data={ atlas } />);
}) }
</Flex>
</CenteredContent>
</Layout>
{ this.state.newAtlasVisible && <FullscreenField /> }
</Flex>
);
}
}
export default Dashboard;

View File

View File

@@ -0,0 +1,27 @@
import { observable, action } from 'mobx';
import { client } from 'utils/ApiClient';
const store = new class DashboardStore {
@observable atlases;
@observable pagination;
@observable isFetching = true;
/* Actions */
@action fetchAtlases = async (teamId) => {
this.isFetching = true;
try {
const res = await client.post('/atlases.list', { id: teamId });
const { data, pagination } = res;
this.atlases = data;
this.pagination = pagination;
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
}();
export default store;

View File

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

View File

@@ -0,0 +1,183 @@
import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { browserHistory, withRouter } from 'react-router';
import keydown from 'react-keydown';
import DocumentEditStore, {
DOCUMENT_EDIT_SETTINGS,
} from './DocumentEditStore';
import Switch from 'components/Switch';
import Layout, { Title, HeaderAction } from 'components/Layout';
import Flex from 'components/Flex';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import DropdownMenu, { MenuItem } from 'components/DropdownMenu';
import EditorLoader from './components/EditorLoader';
import SaveAction from './components/SaveAction';
const DISREGARD_CHANGES = `You have unsaved changes.
Are you sure you want to disgard them?`;
@keydown([
'cmd+enter', 'ctrl+enter',
'cmd+esc', 'ctrl+esc',
'cmd+shift+p', 'ctrl+shift+p'])
@withRouter
@observer
class DocumentEdit extends Component {
static store;
static propTypes = {
route: React.PropTypes.object.isRequired,
router: React.PropTypes.object.isRequired,
params: React.PropTypes.object,
}
state = {
scrollTop: 0,
}
constructor(props) {
super(props);
this.store = new DocumentEditStore(
JSON.parse(localStorage[DOCUMENT_EDIT_SETTINGS] || '{}')
);
}
componentDidMount = () => {
if (this.props.route.newDocument) {
this.store.atlasId = this.props.params.id;
this.store.newDocument = true;
} else if (this.props.route.newChildDocument) {
this.store.documentId = this.props.params.id;
this.store.newChildDocument = true;
this.store.fetchDocument();
} else {
this.store.documentId = this.props.params.id;
this.store.newDocument = false;
this.store.fetchDocument();
}
// Load editor async
EditorLoader()
.then(({ Editor }) => {
this.setState({ Editor });
});
// Set onLeave hook
this.props.router.setRouteLeaveHook(this.props.route, () => {
if (this.store.hasPendingChanges) {
return confirm(DISREGARD_CHANGES);
}
return;
});
}
componentWillReceiveProps = (nextProps) => {
const key = nextProps.keydown.event;
if (key) {
// Cmd + Enter
if(key.key === 'Enter' && (key.metaKey || key.ctrl.Key)) {
this.onSave();
}
// Cmd + Esc
if(key.key === 'Escape' && (key.metaKey || key.ctrl.Key)) {
this.onCancel();
}
// Cmd + m
console.log(key)
if(key.key === 'P' && key.shiftKey && (key.metaKey || key.ctrl.Key)) {
this.store.togglePreview();
}
}
}
onSave = () => {
// if (this.props.title.length === 0) {
// alert("Please add a title before saving (hint: Write a markdown header)");
// return
// }
if (this.store.newDocument || this.store.newChildDocument) {
this.store.saveDocument();
} else {
this.store.updateDocument();
}
}
onCancel = () => {
browserHistory.goBack();
}
onScroll = (scrollTop) => {
this.setState({
scrollTop,
});
}
render() {
console.log("DocumentEdit#render", this.store.preview);
let title = (
<Title
truncate={ 60 }
placeholder={ "Untitle document" }
>
{ this.store.title }
</Title>
);
let titleText = this.store.title;
const actions = (
<Flex direction="row">
<HeaderAction>
<SaveAction
onClick={ this.onSave }
disabled={ this.store.isSaving }
/>
</HeaderAction>
<DropdownMenu label="More">
<MenuItem onClick={ this.store.togglePreview }>
Preview <Switch checked={ this.store.preview } />
</MenuItem>
<MenuItem onClick={ this.onCancel }>
Cancel
</MenuItem>
</DropdownMenu>
</Flex>
);
return (
<Layout
actions={ actions }
title={ title }
titleText={ titleText }
fixed
loading={ this.store.isSaving }
search={ false }
>
{ (this.store.isFetching || !('Editor' in this.state)) ? (
<CenteredContent>
<AtlasPreviewLoading />
</CenteredContent>
) : (
<this.state.Editor
store={ this.store }
scrollTop={ this.state.scrollTop }
onScroll={ this.onScroll }
onSave={ this.onSave }
onCancel={ this.onCancel }
togglePreview={ this.togglePreview }
/>
) }
</Layout>
);
}
}
export default DocumentEdit;

View File

@@ -0,0 +1,46 @@
@import '~styles/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;
height: 100%;
}
.fullWidth {
flex: 1;
display: flex;
.paneContent {
display: flex;
}
}
:global {
::-webkit-scrollbar {
display: none;
}
}

View File

@@ -0,0 +1,147 @@
import { observable, action, toJS, autorun } from 'mobx';
import { client } from 'utils/ApiClient';
import { browserHistory } from 'react-router';
import emojify from 'utils/emojify';
const DOCUMENT_EDIT_SETTINGS = 'DOCUMENT_EDIT_SETTINGS';
const parseHeader = (text) => {
const firstLine = text.split(/\r?\n/)[0];
if (firstLine) {
const match = firstLine.match(/^#+ +(.*)$/);
if (match) {
return emojify(match[1]);
} else {
return '';
}
}
return '';
};
class DocumentEditStore {
@observable documentId = null;
@observable atlasId = null;
@observable parentDocument;
@observable title;
@observable text;
@observable hasPendingChanges = false;
@observable newDocument;
@observable newChildDocument;
@observable preview;
@observable isFetching;
@observable isSaving;
/* Actions */
@action fetchDocument = async () => {
this.isFetching = true;
try {
const data = await client.get('/documents.info', {
id: this.documentId,
});
if (this.newChildDocument) {
this.parentDocument = data.data;
} else {
const { title, text } = data.data;
this.title = title;
this.text = text;
}
} catch (e) {
console.error('Something went wrong');
}
this.isFetching = false;
}
@action saveDocument = async () => {
if (this.isSaving) return;
this.isSaving = true;
try {
const data = await client.post('/documents.create', {
parentDocument: this.parentDocument && this.parentDocument.id,
atlas: this.atlasId || this.parentDocument.atlas.id,
title: this.title,
text: this.text,
});
const { id } = data.data;
this.hasPendingChanges = false;
browserHistory.push(`/documents/${id}`);
} catch (e) {
console.error("Something went wrong");
}
this.isSaving = false;
}
@action updateDocument = async () => {
if (this.isSaving) return;
this.isSaving = true;
try {
await client.post('/documents.update', {
id: this.documentId,
title: this.title,
text: this.text,
});
this.hasPendingChanges = false;
browserHistory.push(`/documents/${this.documentId}`);
} catch (e) {
console.error("Something went wrong");
}
this.isSaving = false;
}
@action updateText = (text) => {
this.text = text;
this.title = parseHeader(text);
this.hasPendingChanges = true;
}
@action updateTitle = (title) => {
this.title = title;
}
@action replaceText = (args) => {
this.text = this.text.replace(args.original, args.new);
this.hasPendingChanges = true;
}
@action togglePreview = () => {
this.preview = !this.preview;
}
@action reset = () => {
this.title = 'Lets start with a title';
this.text = '# Lets start with a title\n\nAnd continue from there...';
}
// Generic
persistSettings = () => {
localStorage[DOCUMENT_EDIT_SETTINGS] = JSON.stringify({
preview: toJS(this.preview),
});
}
constructor(settings) {
// Rehydrate settings
this.preview = settings.preview
// Persist settings to localStorage
// TODO: This could be done more selectively
autorun(() => {
this.persistSettings();
});
}
};
export default DocumentEditStore;
export {
DOCUMENT_EDIT_SETTINGS
};

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { observer } from 'mobx-react';
import { convertToMarkdown } from 'utils/markdown';
import MarkdownEditor from 'components/MarkdownEditor';
import Preview from './Preview';
import EditorPane from './EditorPane';
import styles from '../DocumentEdit.scss';
const Editor = observer((props) => {
const store = props.store;
return (
<div className={ styles.container }>
<EditorPane
fullWidth={ !store.preview }
onScroll={ props.onScroll }
>
<MarkdownEditor
onChange={ store.updateText }
text={ store.text }
replaceText={ store.replaceText }
preview={ store.preview }
onSave={ props.onSave }
onCancel={ props.onCancel }
togglePreview={ props.togglePreview }
/>
</EditorPane>
{ store.preview ? (
<EditorPane
scrollTop={ props.scrollTop }
>
<Preview html={ convertToMarkdown(store.text) } />
</EditorPane>
) : null }
</div>
);
});
export default Editor;

View File

@@ -0,0 +1,9 @@
export default () => {
return new Promise(resolve => {
require.ensure([], () => {
resolve({
Editor: require('./Editor').default,
});
});
});
};

View File

@@ -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 (
<div
className={ cx(styles.editorPane, { fullWidth: this.props.fullWidth }) }
ref="pane"
>
<div ref="content" className={ styles.paneContent }>
{ this.props.children }
</div>
</div>
);
}
};
export default EditorPane;

View File

@@ -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 (
<div className={ styles.preview }>
<DocumentHtml html={ props.html } />
</div>
);
};
Preview.propTypes = {
html: React.PropTypes.string.isRequired,
};
export default Preview;

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { observer } from 'mobx-react';
@observer
class SaveAction extends React.Component {
static propTypes = {
onClick: React.PropTypes.func.isRequired,
disabled: React.PropTypes.bool,
}
onClick = (event) => {
if (this.props.disabled) return;
event.preventDefault();
this.props.onClick();
}
render() {
return (
<div>
<a
href
onClick={ this.onClick }
style={{ opacity: this.props.disabled ? 0.5 : 1 }}
>Save</a>
</div>
);
}
};
export default SaveAction;

View File

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

View File

@@ -0,0 +1,215 @@
import React, { PropTypes } from 'react';
import { toJS } from 'mobx';
import { Link, browserHistory } from 'react-router';
import { observer } from 'mobx-react';
import keydown from 'react-keydown';
import _ from 'lodash';
import DocumentSceneStore, {
DOCUMENT_PREFERENCES
} from './DocumentSceneStore';
import Layout, { HeaderAction } from 'components/Layout';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import CenteredContent from 'components/CenteredContent';
import Document from 'components/Document';
import DropdownMenu, { MenuItem, MoreIcon } from 'components/DropdownMenu';
import Flex from 'components/Flex';
import Tree from 'components/Tree';
import styles from './DocumentScene.scss';
import classNames from 'classnames/bind';
const cx = classNames.bind(styles);
import treeStyles from 'components/Tree/Tree.scss';
@keydown(['cmd+/', 'ctrl+/', 'c', 'e'])
@observer(['ui'])
class DocumentScene extends React.Component {
static propTypes = {
ui: PropTypes.object.isRequired,
}
static store;
state = {
didScroll: false,
}
constructor(props) {
super(props);
this.store = new DocumentSceneStore(JSON.parse(localStorage[DOCUMENT_PREFERENCES] || "{}"));
}
componentDidMount = () => {
const { id } = this.props.routeParams;
this.store.fetchDocument(id);
}
componentWillReceiveProps = (nextProps) => {
const key = nextProps.keydown.event;
if (key) {
if (key.key === '/' && (key.metaKey || key.ctrl.Key)) {
this.toggleSidebar();
}
if (key.key === 'c') {
_.defer(this.onCreate);
}
if (key.key === 'e') {
_.defer(this.onEdit);
}
}
// Reload on url change
const oldId = this.props.params.id;
const newId = nextProps.params.id;
if (oldId !== newId) {
this.store.fetchDocument(newId, true);
}
// Scroll to anchor after loading, and only once
const { hash } = this.props.location;
if (nextProps.doc && hash && !this.state.didScroll) {
const name = hash.split('#')[1];
setTimeout(() => {
this.setState({ didScroll: true });
const element = doc.getElementsByName(name)[0];
if (element) element.scrollIntoView()
}, 0);
}
}
onEdit = () => {
const url = `/documents/${this.store.document.id}/edit`;
browserHistory.push(url);
}
onCreate = () => {
const url = `/documents/${this.store.document.id}/new`;
browserHistory.push(url);
}
onDelete = () => {
let msg;
if (this.store.document.atlas.type === 'atlas') {
msg = 'Are you sure you want to delete this document and all it\'s child documents (if any)?'
} else {
msg = "Are you sure you want to delete this document?";
}
if (confirm(msg)) {
this.store.deleteDocument();
};
}
onExport = () => {
const doc = this.store.document;
const a = document.createElement('a');
a.textContent = 'download';
a.download = `${doc.title}.md`;
a.href = `data:text/markdown;charset=UTF-8,${encodeURIComponent(doc.text)}`;
a.click();
}
toggleSidebar = () => {
this.props.ui.toggleSidebar();
}
renderNode = (node) => {
return (
<span className={ treeStyles.nodeLabel } onClick={this.onClickNode.bind(null, node)}>
{node.module.name}
</span>
);
}
render() {
const {
sidebar,
} = this.props.ui;
const doc = this.store.document;
const allowDelete = doc && doc.atlas.type === 'atlas' &&
doc.id !== doc.atlas.navigationTree.id;
let title;
let titleText;
let actions;
if (doc) {
actions = (
<div className={ styles.actions }>
<DropdownMenu label={ <MoreIcon /> }>
{ this.store.isAtlas && <MenuItem onClick={ this.onCreate }>New document</MenuItem> }
<MenuItem onClick={ this.onEdit }>Edit</MenuItem>
<MenuItem onClick={ this.onExport }>Export</MenuItem>
{ allowDelete && <MenuItem onClick={ this.onDelete }>Delete</MenuItem> }
</DropdownMenu>
</div>
);
title = (
<span>
<Link to={ `/atlas/${doc.atlas.id}` }>{doc.atlas.name}</Link>
{ ` / ${doc.title}` }
</span>
);
titleText = `${doc.atlas.name} - ${doc.title}`;
}
return (
<Layout
title={ title }
titleText={ titleText }
actions={ doc && actions }
loading={ this.store.updatingStructure }
>
{ this.store.isFetching ? (
<CenteredContent>
<AtlasPreviewLoading />
</CenteredContent>
) : (
<Flex flex={ true }>
{ this.store.isAtlas && (
<Flex>
{ sidebar && (
<div className={ styles.sidebar }>
<Tree
paddingLeft={ 10 }
tree={ toJS(this.store.atlasTree) }
onChange={ this.store.updateNavigationTree }
onCollapse={ this.store.onNodeCollapse }
isNodeCollapsed={ this.isNodeCollapsed }
renderNode={ this.renderNode }
/>
</div>
) }
<div
onClick={ this.toggleSidebar }
className={ styles.sidebarToggle }
title="Toggle sidebar (Cmd+/)"
>
<img
src={ require("assets/icons/menu.svg") }
className={ styles.menuIcon }
/>
</div>
</Flex>
) }
<Flex flex={ true } justify={ 'center' } className={ styles.content}>
<CenteredContent>
{ this.store.updatingContent ? (
<AtlasPreviewLoading />
) : (
<Document document={ doc } />
) }
</CenteredContent>
</Flex>
</Flex>
) }
</Layout>
);
}
};
export default DocumentScene;

View File

@@ -0,0 +1,37 @@
.actions {
display: flex;
flex-direction: row;
}
.sidebar {
width: 250px;
padding: 20px 20px 20px 5px;
border-right: 1px solid #eee;
}
.content {
position: relative;
}
.sidebarToggle {
display: flex;
position: relative;
width: 60px;
cursor: pointer;
justify-content: center;
&:hover {
background-color: #f5f5f5;
.menuIcon {
opacity: 1;
}
}
}
.menuIcon {
margin-top: 18px;
height: 28px;
opacity: 0.15;
}

View File

@@ -0,0 +1,129 @@
import _isEqual from 'lodash/isEqual';
import _indexOf from 'lodash/indexOf';
import _without from 'lodash/without';
import { observable, action, computed, runInAction, toJS, autorun } from 'mobx';
import { client } from 'utils/ApiClient';
import { browserHistory } from 'react-router';
const DOCUMENT_PREFERENCES = 'DOCUMENT_PREFERENCES';
class DocumentSceneStore {
@observable document;
@observable collapsedNodes = [];
@observable isFetching = true;
@observable updatingContent = false;
@observable updatingStructure = false;
@observable isDeleting;
/* Computed */
@computed get isAtlas() {
return this.document &&
this.document.atlas.type === 'atlas';
}
@computed get atlasTree() {
if (this.document.atlas.type !== 'atlas') return;
let tree = this.document.atlas.navigationTree;
const collapseNodes = (node) => {
if (this.collapsedNodes.includes(node.id)) {
node.collapsed = true;
}
node.children = node.children.map(childNode => {
return collapseNodes(childNode);
})
return node;
};
return collapseNodes(toJS(tree));
}
/* Actions */
@action fetchDocument = async (id, softLoad) => {
this.isFetching = !softLoad;
this.updatingContent = softLoad;
try {
const res = await client.get('/documents.info', { id: id });
const { data } = res;
runInAction('fetchDocument', () => {
this.document = data;
});
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
this.updatingContent = false;
}
@action deleteDocument = async () => {
this.isFetching = true;
try {
const res = await client.post('/documents.delete', { id: this.document.id });
browserHistory.push(`/atlas/${this.document.atlas.id}`);
} catch (e) {
console.error("Something went wrong");
}
this.isFetching = false;
}
@action updateNavigationTree = async (tree) => {
// Only update when tree changes
if (_isEqual(toJS(tree), toJS(this.document.atlas.navigationTree))) {
return true;
}
this.updatingStructure = true;
try {
const res = await client.post('/atlases.updateNavigationTree', {
id: this.document.atlas.id,
tree: tree,
});
runInAction('updateNavigationTree', () => {
const { data } = res;
this.document.atlas = data;
});
} catch (e) {
console.error("Something went wrong");
}
this.updatingStructure = false;
}
@action onNodeCollapse = (nodeId, collapsed) => {
if (_indexOf(this.collapsedNodes, nodeId) >= 0) {
this.collapsedNodes = _without(this.collapsedNodes, nodeId);
} else {
this.collapsedNodes.push(nodeId);
}
}
// General
persistSettings = () => {
localStorage[DOCUMENT_PREFERENCES] = JSON.stringify({
collapsedNodes: toJS(this.collapsedNodes),
});
}
constructor(settings) {
// Rehydrate settings
this.collapsedNodes = settings.collapsedNodes || [];
// Persist settings to localStorage
// TODO: This could be done more selectively
autorun(() => {
this.persistSettings();
});
}
};
export default DocumentSceneStore;
export {
DOCUMENT_PREFERENCES,
};

View File

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

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { observer } from 'mobx-react';
import { browserHistory } from 'react-router'
import SlackAuthLink from 'components/SlackAuthLink';
import styles from './Home.scss';
@observer(['user'])
export default class Home extends React.Component {
static propTypes = {
user: React.PropTypes.object.isRequired,
}
componentDidMount = () => {
if (this.props.user.authenticated) {
browserHistory.replace('/dashboard');
}
}
render() {
return (
<div className={ styles.container }>
<div className={ styles.content }>
<div className={ styles.intro }>
<p>
Hi there,
</p>
<p>
We're building the best place for engineers, designers and teams to
share ideas, tell stories and build knowledge.
</p>
<p>
<strong>**Atlas**</strong> can start as a wiki, but it's really
up to you what you want to make of it:
</p>
<p>
- Write documentation in <i>_markdown_</i><br/>
- Build a blog around the API<br/>
- Hack the frontend for your needs (coming!)<br/>
</p>
<p>
We're just getting started.
</p>
</div>
<div className={ styles.action }>
<SlackAuthLink />
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,17 @@
.container {
display: flex;
flex: 1;
justify-content: space-around;
}
.content {
margin: 40px;
max-width: 640px;
}
.intro {
font-family: "Atlas Typewriter", Monaco, monospace;
font-size: 1.4em;
line-height: 1.6em;
margin-bottom: 40px;
}

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Frame } from 'react-keyframes';
let frames = [];
const p = (node) => frames.push(node);
const E = (props) => {
return (<Frame duration={props.duration || 300} component='div'>{ props.children }</Frame>);
};
const line1 = (<p>Hi there,</p>);
const line2 = (<p>We're excited to share what were building.</p>);
const line3 = (<p>We <strong>**love**</strong> Markdown,</p>);
const line4 = (<p>but we also get that it's not for everyone.</p>);
const line5 = (<p>Together with you,</p>);
const line6 = (<p>we want to build the best place to</p>);
const line7 = (<p>share ideas,</p>);
const line8 = (<p>tell stories,</p>);
const line9 = (<p>and build knowledge.</p>);
const line10 = (<p>We're just getting started.</p>);
const line11 = (<p>Welcome to Beautiful Atlas.</p>);
p(<E>{line1}{line2}{line3}{line4}{line5}{line6}{line7}{line8}{line9}{line10}{line11}</E>);
// Hmms leaving this here for now, would be nice to something
export default frames;

View File

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

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { observer } from 'mobx-react';
import _debounce from 'lodash/debounce';
import Flex from 'components/Flex';
import Layout from 'components/Layout';
import CenteredContent from 'components/CenteredContent';
import SearchField from './components/SearchField';
import DocumentPreview from 'components/DocumentPreview';
import AtlasPreviewLoading from 'components/AtlasPreviewLoading';
import styles from './Search.scss';
import SearchStore from './SearchStore';
@observer
class Search extends React.Component {
static store;
constructor(props) {
super(props);
this.store = new SearchStore();
}
render() {
const search = _debounce((searchTerm) => {
this.store.search(searchTerm);
}, 250);
return (
<Layout
title="Search"
titleText="Search"
search={ false }
loading={ this.store.isFetching }
>
<CenteredContent>
<Flex direction="column" flex={ true }>
<Flex flex={ true }>
<img
src={ require('assets/icons/search.svg') }
className={ styles.icon }
/>
<SearchField
searchTerm={ this.store.searchTerm }
onChange={ search }
/>
</Flex>
{ this.store.documents && this.store.documents.map((document) => {
return (<DocumentPreview key={ document.id } document={ document } />);
}) }
</Flex>
</CenteredContent>
</Layout>
);
}
}
export default Search;

View File

@@ -0,0 +1,6 @@
.icon {
width: 38px;
margin-bottom: -5px;
margin-right: 10px;
opacity: 0.15;
}

View File

@@ -0,0 +1,39 @@
import { observable, action, runInAction } from 'mobx';
import { client } from 'utils/ApiClient';
import { browserHistory } from 'react-router';
class SearchStore {
@observable documents;
@observable pagination;
@observable selectedDocument;
@observable searchTerm;
@observable isFetching = false;
/* Actions */
@action search = async (query) => {
this.searchTerm = query;
this.isFetching = true;
if (query) {
try {
const res = await client.get('/documents.search', { query });
const { data, pagination } = res;
runInAction('search document', () => {
this.documents = data;
this.pagination = pagination;
});
} catch (e) {
console.error("Something went wrong");
}
} else {
this.documents = null;
}
this.isFetching = false;
}
};
export default SearchStore;

View File

@@ -0,0 +1,32 @@
import React, { PropTypes } from 'react';
import { observer } from 'mobx-react';
import Flex from 'components/Flex';
import styles from './SearchField.scss';
@observer
class SearchField extends React.Component {
static propTypes = {
onChange: PropTypes.func,
}
onChange = (event) => {
this.props.onChange(event.currentTarget.value);
}
render() {
return (
<div className={ styles.container }>
<input
onChange={ this.onChange }
className={ styles.field }
placeholder="Search"
autoFocus
/>
</div>
);
}
}
export default SearchField;

View File

@@ -0,0 +1,31 @@
.container {
padding: 40px 0;
}
.field {
width: 100%;
padding: 10px;
font-size: 48px;
font-weight: 400;
outline: none;
border: 0;
// border-bottom: 1px solid #ccc;
}
:global {
::-webkit-input-placeholder {
color: #ccc;
}
:-moz-placeholder {
color: #ccc;
}
::-moz-placeholder {
color: #ccc;
}
:-ms-input-placeholder {
color: #ccc;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,22 @@
import React from 'react';
import { observer } from 'mobx-react';
@observer(['user'])
class SlackAuth extends React.Component {
static propTypes = {
user: React.PropTypes.object.isRequired,
}
componentDidMount = () => {
const { code, state } = this.props.location.query;
this.props.user.authWithSlack(code, state);
}
render() {
return (
<div></div>
);
}
}
export default SlackAuth;

View File

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