Renamed /src to /frontend
This commit is contained in:
23
frontend/scenes/Application.js
Normal file
23
frontend/scenes/Application.js
Normal 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;
|
||||
96
frontend/scenes/Atlas/Atlas.js
Normal file
96
frontend/scenes/Atlas/Atlas.js
Normal 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;
|
||||
17
frontend/scenes/Atlas/Atlas.scss
Normal file
17
frontend/scenes/Atlas/Atlas.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
27
frontend/scenes/Atlas/AtlasStore.js
Normal file
27
frontend/scenes/Atlas/AtlasStore.js
Normal 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;
|
||||
2
frontend/scenes/Atlas/index.js
Normal file
2
frontend/scenes/Atlas/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Atlas from './Atlas';
|
||||
export default Atlas;
|
||||
69
frontend/scenes/Dashboard/Dashboard.js
Normal file
69
frontend/scenes/Dashboard/Dashboard.js
Normal 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;
|
||||
0
frontend/scenes/Dashboard/Dashboard.scss
Normal file
0
frontend/scenes/Dashboard/Dashboard.scss
Normal file
27
frontend/scenes/Dashboard/DashboardStore.js
Normal file
27
frontend/scenes/Dashboard/DashboardStore.js
Normal 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;
|
||||
2
frontend/scenes/Dashboard/index.js
Normal file
2
frontend/scenes/Dashboard/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Dashboard from './Dashboard';
|
||||
export default Dashboard;
|
||||
183
frontend/scenes/DocumentEdit/DocumentEdit.js
Normal file
183
frontend/scenes/DocumentEdit/DocumentEdit.js
Normal 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;
|
||||
46
frontend/scenes/DocumentEdit/DocumentEdit.scss
Normal file
46
frontend/scenes/DocumentEdit/DocumentEdit.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
147
frontend/scenes/DocumentEdit/DocumentEditStore.js
Normal file
147
frontend/scenes/DocumentEdit/DocumentEditStore.js
Normal 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
|
||||
};
|
||||
41
frontend/scenes/DocumentEdit/components/Editor.js
Normal file
41
frontend/scenes/DocumentEdit/components/Editor.js
Normal 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;
|
||||
9
frontend/scenes/DocumentEdit/components/EditorLoader.js
Normal file
9
frontend/scenes/DocumentEdit/components/EditorLoader.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export default () => {
|
||||
return new Promise(resolve => {
|
||||
require.ensure([], () => {
|
||||
resolve({
|
||||
Editor: require('./Editor').default,
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
62
frontend/scenes/DocumentEdit/components/EditorPane.js
Normal file
62
frontend/scenes/DocumentEdit/components/EditorPane.js
Normal 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;
|
||||
21
frontend/scenes/DocumentEdit/components/Preview.js
Normal file
21
frontend/scenes/DocumentEdit/components/Preview.js
Normal 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;
|
||||
31
frontend/scenes/DocumentEdit/components/SaveAction.js
Normal file
31
frontend/scenes/DocumentEdit/components/SaveAction.js
Normal 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;
|
||||
2
frontend/scenes/DocumentEdit/index.js
Normal file
2
frontend/scenes/DocumentEdit/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import DocumentEdit from './DocumentEdit';
|
||||
export default DocumentEdit;
|
||||
215
frontend/scenes/DocumentScene/DocumentScene.js
Normal file
215
frontend/scenes/DocumentScene/DocumentScene.js
Normal 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;
|
||||
37
frontend/scenes/DocumentScene/DocumentScene.scss
Normal file
37
frontend/scenes/DocumentScene/DocumentScene.scss
Normal 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;
|
||||
}
|
||||
129
frontend/scenes/DocumentScene/DocumentSceneStore.js
Normal file
129
frontend/scenes/DocumentScene/DocumentSceneStore.js
Normal 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,
|
||||
};
|
||||
2
frontend/scenes/DocumentScene/index.js
Normal file
2
frontend/scenes/DocumentScene/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import DocumentScene from './DocumentScene';
|
||||
export default DocumentScene;
|
||||
53
frontend/scenes/Home/Home.js
Normal file
53
frontend/scenes/Home/Home.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
17
frontend/scenes/Home/Home.scss
Normal file
17
frontend/scenes/Home/Home.scss
Normal 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;
|
||||
}
|
||||
26
frontend/scenes/Home/animation.js
Normal file
26
frontend/scenes/Home/animation.js
Normal 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 we’re 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;
|
||||
2
frontend/scenes/Home/index.js
Normal file
2
frontend/scenes/Home/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Home from './Home';
|
||||
export default Home;
|
||||
59
frontend/scenes/Search/Search.js
Normal file
59
frontend/scenes/Search/Search.js
Normal 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;
|
||||
6
frontend/scenes/Search/Search.scss
Normal file
6
frontend/scenes/Search/Search.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.icon {
|
||||
width: 38px;
|
||||
margin-bottom: -5px;
|
||||
margin-right: 10px;
|
||||
opacity: 0.15;
|
||||
}
|
||||
39
frontend/scenes/Search/SearchStore.js
Normal file
39
frontend/scenes/Search/SearchStore.js
Normal 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;
|
||||
32
frontend/scenes/Search/components/SearchField/SearchField.js
Normal file
32
frontend/scenes/Search/components/SearchField/SearchField.js
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
2
frontend/scenes/Search/components/SearchField/index.js
Normal file
2
frontend/scenes/Search/components/SearchField/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import SearchField from './SearchField';
|
||||
export default SearchField;
|
||||
2
frontend/scenes/Search/index.js
Normal file
2
frontend/scenes/Search/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Search from './Search';
|
||||
export default Search;
|
||||
22
frontend/scenes/SlackAuth/SlackAuth.js
Normal file
22
frontend/scenes/SlackAuth/SlackAuth.js
Normal 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;
|
||||
2
frontend/scenes/SlackAuth/index.js
Normal file
2
frontend/scenes/SlackAuth/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import SlackAuth from './SlackAuth';
|
||||
export default SlackAuth;
|
||||
Reference in New Issue
Block a user