diff --git a/app/components/CenteredContent/CenteredContent.js b/app/components/CenteredContent/CenteredContent.js index 578d0055..bcfaedb8 100644 --- a/app/components/CenteredContent/CenteredContent.js +++ b/app/components/CenteredContent/CenteredContent.js @@ -12,7 +12,7 @@ const Container = styled.div` `; const Content = styled.div` - max-width: 50em; + max-width: 46em; margin: 0 auto; `; diff --git a/app/components/Editor/Editor.js b/app/components/Editor/Editor.js index 50a5805f..4e4d7129 100644 --- a/app/components/Editor/Editor.js +++ b/app/components/Editor/Editor.js @@ -228,8 +228,8 @@ class MarkdownEditor extends Component { } const MaxWidth = styled(Flex)` - padding: 0 60px; - max-width: 50em; + margin: 0 60px; + max-width: 46em; height: 100%; `; @@ -281,6 +281,8 @@ const StyledEditor = styled(Editor)` p { position: relative; + margin-top: 1.2em; + margin-bottom: 1.2em; } a:hover { diff --git a/app/components/Icon/CollectionIcon.js b/app/components/Icon/CollectionIcon.js index f419be0d..25182419 100644 --- a/app/components/Icon/CollectionIcon.js +++ b/app/components/Icon/CollectionIcon.js @@ -6,7 +6,7 @@ import type { Props } from './Icon'; export default function CollectionIcon({ expanded, ...rest -}: Props & { expanded: boolean }) { +}: Props & { expanded?: boolean }) { return ( {expanded ? ( diff --git a/app/components/Subheading/Subheading.js b/app/components/Subheading/Subheading.js new file mode 100644 index 00000000..fdc71648 --- /dev/null +++ b/app/components/Subheading/Subheading.js @@ -0,0 +1,15 @@ +// @flow +import styled from 'styled-components'; + +const Subheading = styled.h3` + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + color: #9fa6ab; + letter-spacing: 0.04em; + border-bottom: 1px solid #ddd; + padding-bottom: 10px; + margin-top: 30px; +`; + +export default Subheading; diff --git a/app/components/Subheading/index.js b/app/components/Subheading/index.js new file mode 100644 index 00000000..d0f6a76e --- /dev/null +++ b/app/components/Subheading/index.js @@ -0,0 +1,3 @@ +// @flow +import Subheading from './Subheading'; +export default Subheading; diff --git a/app/models/Collection.js b/app/models/Collection.js index 96a1c57e..e7a4013f 100644 --- a/app/models/Collection.js +++ b/app/models/Collection.js @@ -39,6 +39,11 @@ class Collection extends BaseModel { return true; } + @computed + get isEmpty(): boolean { + return this.documents.length === 0; + } + /* Actions */ @action diff --git a/app/scenes/Collection/Collection.js b/app/scenes/Collection/Collection.js index 193e58de..6cd13ab7 100644 --- a/app/scenes/Collection/Collection.js +++ b/app/scenes/Collection/Collection.js @@ -2,20 +2,26 @@ import React, { Component } from 'react'; import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { Link, Redirect } from 'react-router-dom'; +import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { newDocumentUrl } from 'utils/routeHelpers'; import CollectionsStore from 'stores/CollectionsStore'; +import UiStore from 'stores/UiStore'; import Collection from 'models/Collection'; +import Search from 'scenes/Search'; import CenteredContent from 'components/CenteredContent'; +import CollectionIcon from 'components/Icon/CollectionIcon'; import LoadingListPlaceholder from 'components/LoadingListPlaceholder'; import Button from 'components/Button'; import HelpText from 'components/HelpText'; +import Subheading from 'components/Subheading'; +import PageTitle from 'components/PageTitle'; import Flex from 'shared/components/Flex'; type Props = { + ui: UiStore, collections: CollectionsStore, match: Object, }; @@ -24,29 +30,27 @@ type Props = { class CollectionScene extends Component { props: Props; @observable collection: ?Collection; - @observable isFetching = true; - @observable redirectUrl; + @observable isFetching: boolean = true; componentDidMount = () => { - this.fetchDocument(this.props.match.params.id); + this.fetchCollection(this.props.match.params.id); }; componentWillReceiveProps(nextProps) { if (nextProps.match.params.id !== this.props.match.params.id) { - this.fetchDocument(nextProps.match.params.id); + this.fetchCollection(nextProps.match.params.id); } } - fetchDocument = async (id: string) => { + fetchCollection = async (id: string) => { const { collections } = this.props; - this.collection = await collections.fetchById(id); - - if (!this.collection) this.redirectUrl = '/404'; - - if (this.collection && this.collection.documents.length > 0) { - this.redirectUrl = this.collection.documents[0].url; + const collection = await collections.fetch(id); + if (collection) { + this.props.ui.setActiveCollection(collection); + this.collection = collection; } + this.isFetching = false; }; @@ -54,43 +58,57 @@ class CollectionScene extends Component { if (!this.collection) return; return ( - -

Create a document

+ + + + {' '} + {this.collection.name} + - Publish your first document to start building the{' '} - {this.collection.name} collection. + Publish your first document to start building this collection. -
+ ); } + renderNotFound() { + return ; + } + render() { - if (this.redirectUrl) return ; + if (this.isFetching) return ; + if (!this.collection) return this.renderNotFound(); + if (this.collection.isEmpty) return this.renderEmptyCollection(); return ( - {this.isFetching ? ( - - ) : ( - this.renderEmptyCollection() - )} + + + {' '} + {this.collection.name} + + Recently edited ); } } -const NewDocumentContainer = styled(Flex)` - padding-top: 50%; - transform: translateY(-50%); +const Heading = styled.h1` + display: flex; + + svg { + margin-left: -6px; + margin-right: 6px; + } `; const Action = styled(Flex)` margin: 10px 0; `; -export default inject('collections')(CollectionScene); +export default inject('collections', 'ui')(CollectionScene); diff --git a/app/scenes/Dashboard/Dashboard.js b/app/scenes/Dashboard/Dashboard.js index b33042fc..1eb9d07f 100644 --- a/app/scenes/Dashboard/Dashboard.js +++ b/app/scenes/Dashboard/Dashboard.js @@ -4,11 +4,10 @@ import { observable } from 'mobx'; import { observer, inject } from 'mobx-react'; import DocumentsStore from 'stores/DocumentsStore'; -import Flex from 'shared/components/Flex'; +import CenteredContent from 'components/CenteredContent'; import DocumentList from 'components/DocumentList'; import PageTitle from 'components/PageTitle'; import Subheading from 'components/Subheading'; -import CenteredContent from 'components/CenteredContent'; import { ListPlaceholder } from 'components/LoadingPlaceholder'; type Props = { @@ -34,30 +33,26 @@ class Dashboard extends Component { render() { const { documents } = this.props; - const recentlyViewedLoaded = documents.recentlyViewed.length > 0; - const recentlyEditedLoaded = documents.recentlyEdited.length > 0; + const hasRecentlyViewed = documents.recentlyViewed.length > 0; + const hasRecentlyEdited = documents.recentlyEdited.length > 0; const showContent = - this.isLoaded || (recentlyViewedLoaded && recentlyEditedLoaded); + this.isLoaded || (hasRecentlyViewed && hasRecentlyEdited); return (

Home

{showContent ? ( - - {recentlyViewedLoaded && ( - - Recently viewed - - - )} - {recentlyEditedLoaded && ( - - Recently edited - - - )} - + + {hasRecentlyViewed && [ + Recently viewed, + , + ]} + {hasRecentlyEdited && [ + Recently edited, + , + ]} + ) : ( )} diff --git a/app/scenes/Document/Document.js b/app/scenes/Document/Document.js index cdcc83fe..9fb48a94 100644 --- a/app/scenes/Document/Document.js +++ b/app/scenes/Document/Document.js @@ -44,7 +44,6 @@ type Props = { match: Object, history: Object, location: Location, - keydown: Object, documents: DocumentsStore, collections: CollectionsStore, newDocument?: boolean, diff --git a/app/stores/CollectionsStore.js b/app/stores/CollectionsStore.js index 18b205af..18e7c2ff 100644 --- a/app/stores/CollectionsStore.js +++ b/app/stores/CollectionsStore.js @@ -1,11 +1,5 @@ // @flow -import { - observable, - computed, - action, - runInAction, - ObservableArray, -} from 'mobx'; +import { observable, computed, action, runInAction, ObservableMap } from 'mobx'; import ApiClient, { client } from 'utils/ApiClient'; import _ from 'lodash'; import invariant from 'invariant'; @@ -13,12 +7,10 @@ import invariant from 'invariant'; import stores from 'stores'; import Collection from 'models/Collection'; import ErrorsStore from 'stores/ErrorsStore'; -import CacheStore from 'stores/CacheStore'; import UiStore from 'stores/UiStore'; type Options = { teamId: string, - cache: CacheStore, ui: UiStore, }; @@ -30,17 +22,17 @@ type DocumentPathItem = { }; export type DocumentPath = DocumentPathItem & { - path: Array, + path: DocumentPathItem[], }; class CollectionsStore { - @observable data: ObservableArray = observable.array([]); + @observable data: Map = new ObservableMap([]); @observable isLoaded: boolean = false; + @observable isFetching: boolean = false; client: ApiClient; teamId: string; errors: ErrorsStore; - cache: CacheStore; ui: UiStore; @computed @@ -52,7 +44,7 @@ class CollectionsStore { @computed get orderedData(): Collection[] { - return _.sortBy(this.data, 'name'); + return _.sortBy(this.data.values(), 'name'); } /** @@ -100,6 +92,8 @@ class CollectionsStore { @action fetchAll = async (): Promise<*> => { + this.isFetching = true; + try { const res = await this.client.post('/collections.list', { id: this.teamId, @@ -107,56 +101,64 @@ class CollectionsStore { invariant(res && res.data, 'Collection list not available'); const { data } = res; runInAction('CollectionsStore#fetch', () => { - this.data.replace(data.map(collection => new Collection(collection))); + data.forEach(collection => { + this.data.set(collection.id, new Collection(collection)); + }); this.isLoaded = true; }); } catch (e) { this.errors.add('Failed to load collections'); + } finally { + this.isFetching = false; } }; @action - fetchById = async (id: string): Promise => { + fetch = async (id: string): Promise => { let collection = this.getById(id); - if (!collection) { - try { - const res = await this.client.post('/collections.info', { - id, - }); - invariant(res && res.data, 'Collection not available'); - const { data } = res; - runInAction('CollectionsStore#getById', () => { - collection = new Collection(data); - this.add(collection); - }); - } catch (e) { - Bugsnag.notify(e); - this.errors.add('Something went wrong'); - } - } + if (collection) return collection; - return collection; + this.isFetching = true; + + try { + const res = await this.client.post('/collections.info', { + id, + }); + invariant(res && res.data, 'Collection not available'); + const { data } = res; + const collection = new Collection(data); + + runInAction('CollectionsStore#fetch', () => { + this.data.set(data.id, collection); + this.isLoaded = true; + }); + + return collection; + } catch (e) { + this.errors.add('Something went wrong'); + } finally { + this.isFetching = false; + } }; @action add = (collection: Collection): void => { - this.data.push(collection); + this.data.set(collection.id, collection); }; @action remove = (id: string): void => { - this.data.splice(this.data.indexOf(id), 1); + this.data.delete(id); }; getById = (id: string): ?Collection => { - return _.find(this.data, { id }); + return this.data.get(id); }; constructor(options: Options) { this.client = client; this.errors = stores.errors; this.teamId = options.teamId; - this.cache = options.cache; this.ui = options.ui; } } diff --git a/app/stores/UiStore.js b/app/stores/UiStore.js index 10acfe2f..3fc05269 100644 --- a/app/stores/UiStore.js +++ b/app/stores/UiStore.js @@ -1,6 +1,7 @@ // @flow import { observable, action } from 'mobx'; import Document from 'models/Document'; +import Collection from 'models/Collection'; class UiStore { @observable activeModalName: ?string; @@ -29,6 +30,11 @@ class UiStore { this.activeCollectionId = document.collection.id; }; + @action + setActiveCollection = (collection: Collection): void => { + this.activeCollectionId = collection.id; + }; + @action clearActiveDocument = (): void => { this.activeDocumentId = undefined; diff --git a/shared/styles/constants.js b/shared/styles/constants.js index 3a149656..303d6bee 100644 --- a/shared/styles/constants.js +++ b/shared/styles/constants.js @@ -45,7 +45,7 @@ export const color = { text: '#171B35', /* Brand */ - primary: '#2B8FBF', + primary: '#1AB6FF', danger: '#D0021B', warning: '#f08a24' /* replace */, success: '#43AC6A' /* replace */,