WIP: Collection home

This commit is contained in:
Tom Moor
2017-11-19 20:16:49 -08:00
parent 505310c172
commit 2143a87671
12 changed files with 133 additions and 88 deletions

View File

@@ -12,7 +12,7 @@ const Container = styled.div`
`;
const Content = styled.div`
max-width: 50em;
max-width: 46em;
margin: 0 auto;
`;

View File

@@ -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 {

View File

@@ -6,7 +6,7 @@ import type { Props } from './Icon';
export default function CollectionIcon({
expanded,
...rest
}: Props & { expanded: boolean }) {
}: Props & { expanded?: boolean }) {
return (
<Icon {...rest}>
{expanded ? (

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
// @flow
import Subheading from './Subheading';
export default Subheading;

View File

@@ -39,6 +39,11 @@ class Collection extends BaseModel {
return true;
}
@computed
get isEmpty(): boolean {
return this.documents.length === 0;
}
/* Actions */
@action

View File

@@ -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 (
<NewDocumentContainer auto column justify="center">
<h1>Create a document</h1>
<CenteredContent>
<PageTitle title={this.collection.name} />
<Heading>
<CollectionIcon color={this.collection.color} size={40} expanded />{' '}
{this.collection.name}
</Heading>
<HelpText>
Publish your first document to start building the{' '}
<strong>{this.collection.name}</strong> collection.
Publish your first document to start building this collection.
</HelpText>
<Action>
<Link to={newDocumentUrl(this.collection)}>
<Button>Create new document</Button>
</Link>
</Action>
</NewDocumentContainer>
</CenteredContent>
);
}
renderNotFound() {
return <Search notFound />;
}
render() {
if (this.redirectUrl) return <Redirect to={this.redirectUrl} />;
if (this.isFetching) return <LoadingListPlaceholder />;
if (!this.collection) return this.renderNotFound();
if (this.collection.isEmpty) return this.renderEmptyCollection();
return (
<CenteredContent>
{this.isFetching ? (
<LoadingListPlaceholder />
) : (
this.renderEmptyCollection()
)}
<PageTitle title={this.collection.name} />
<Heading>
<CollectionIcon color={this.collection.color} size={40} expanded />{' '}
{this.collection.name}
</Heading>
<Subheading>Recently edited</Subheading>
</CenteredContent>
);
}
}
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);

View File

@@ -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 (
<CenteredContent>
<PageTitle title="Home" />
<h1>Home</h1>
{showContent ? (
<Flex column>
{recentlyViewedLoaded && (
<Flex column>
<Subheading>Recently viewed</Subheading>
<DocumentList documents={documents.recentlyViewed} />
</Flex>
)}
{recentlyEditedLoaded && (
<Flex column>
<Subheading>Recently edited</Subheading>
<DocumentList documents={documents.recentlyEdited} />
</Flex>
)}
</Flex>
<span>
{hasRecentlyViewed && [
<Subheading>Recently viewed</Subheading>,
<DocumentList documents={documents.recentlyViewed} />,
]}
{hasRecentlyEdited && [
<Subheading>Recently edited</Subheading>,
<DocumentList documents={documents.recentlyEdited} />,
]}
</span>
) : (
<ListPlaceholder count={5} />
)}

View File

@@ -44,7 +44,6 @@ type Props = {
match: Object,
history: Object,
location: Location,
keydown: Object,
documents: DocumentsStore,
collections: CollectionsStore,
newDocument?: boolean,

View File

@@ -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<DocumentPathItem>,
path: DocumentPathItem[],
};
class CollectionsStore {
@observable data: ObservableArray<Collection> = observable.array([]);
@observable data: Map<string, Collection> = 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<?Collection> => {
fetch = async (id: string): Promise<?Collection> => {
let collection = this.getById(id);
if (!collection) {
if (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;
runInAction('CollectionsStore#getById', () => {
collection = new Collection(data);
this.add(collection);
const collection = new Collection(data);
runInAction('CollectionsStore#fetch', () => {
this.data.set(data.id, collection);
this.isLoaded = true;
});
} catch (e) {
Bugsnag.notify(e);
this.errors.add('Something went wrong');
}
}
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;
}
}

View File

@@ -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;

View File

@@ -45,7 +45,7 @@ export const color = {
text: '#171B35',
/* Brand */
primary: '#2B8FBF',
primary: '#1AB6FF',
danger: '#D0021B',
warning: '#f08a24' /* replace */,
success: '#43AC6A' /* replace */,