Skip to content

Commit

Permalink
Implement propel router
Browse files Browse the repository at this point in the history
and some fixes in components
  • Loading branch information
qti3e authored and piscisaureus committed May 18, 2018
1 parent db592b5 commit 61f8610
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 66 deletions.
205 changes: 205 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*!
Copyright 2018 Propel http://propel.site/. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { Component, h } from "preact";
import * as db from "./db";
import { pushState, Router } from "./router";
import * as types from "./types";
import { equal } from "./util";

import { ErrorPage } from "./components/error";
import { GlobalHeader } from "./components/header";
import { Loading } from "./components/loading";
import { UserMenu } from "./components/menu";
import { Notebook } from "./components/notebook";
import { Profile } from "./components/profile";
import { Recent } from "./components/recent";

interface BindProps {
[key: string]: (props: any) => Promise<any>;
}

interface BindState {
data: { [key: string]: string };
error: string;
}

/**
* This react HOC can be used to bind result of some async
* methods to props of the given component (C).
* see: https://reactjs.org/docs/higher-order-components.html
*
* const newComponent = bind(Component, {
* async prop(props) {
* const re = await someAsyncActions();
* return re;
* }
* });
*/
function bind(C, bindProps: BindProps) {
return class extends Component<any, BindState> {
state = { data: null, error: null };
prevMatches = null;

async loadData() {
if (equal(this.props.matches, this.prevMatches)) return;
this.prevMatches = this.props.matches;
const data = {};
for (const key in bindProps) {
if (!bindProps[key]) continue;
try {
data[key] = await bindProps[key](this.props);
} catch (e) {
this.setState({ data: null, error: e.message });
return;
}
}
this.setState({ data, error: null });
}

render() {
this.loadData();
const { data, error } = this.state;
if (error) return <ErrorPage message={error} />;
if (!data) return <Loading />;
return <C {...this.props} {...data} />;
}
};
}

// An anonymous notebook doc for when users aren't logged in
const anonDoc = {
anonymous: true,
cells: [],
created: new Date(),
owner: {
displayName: "Anonymous",
photoURL: require("./img/anon_profile.png"),
uid: ""
},
title: "Anonymous Notebook",
updated: new Date()
};

// TODO Move these components to ./pages.tsx.
// tslint:disable:variable-name
async function onNewNotebookClicked() {
const nbId = await db.active.create();
// Redirect to new notebook.
pushState(`/notebook/${nbId}`);
}

async function onPreviewClicked(nbId: string) {
// Redirect to notebook.
pushState(`/notebook/${nbId}`);
}

export const RecentPage = bind(Recent, {
mostRecent() {
return db.active.queryLatest();
},
async onClick() {
return (nbId: string) => onPreviewClicked(nbId);
},
async onNewNotebookClicked() {
return () => onNewNotebookClicked();
}
});

export const ProfilePage = bind(Profile, {
notebooks(props) {
const uid = props.matches.userId;
return db.active.queryProfile(uid, 100);
},
async onClick() {
return (nbId: string) => onPreviewClicked(nbId);
},
async onNewNotebookClicked() {
return () => onNewNotebookClicked();
}
});

export const NotebookPage = bind(Notebook, {
initialDoc(props) {
const nbId = props.matches.nbId;
return nbId === "anonymous"
? Promise.resolve(anonDoc)
: db.active.getDoc(nbId);
},
save(props) {
const nbId = props.matches.nbId;
const cb = async doc => {
if (doc.anonymous) return;
if (!props.userInfo) return;
if (props.userInfo.uid !== doc.owner.uid) return;
try {
await db.active.updateDoc(nbId, doc);
} catch (e) {
// TODO
console.log(e);
}
};
return Promise.resolve(cb);
},
clone(props) {
const cb = async doc => {
const cloneId = await db.active.clone(doc);
// Redirect to new notebook.
pushState(`/notebook/${cloneId}`);
};
return Promise.resolve(cb);
}
});
// tslint:enable:variable-name

export interface AppState {
loadingAuth: boolean;
userInfo: types.UserInfo;
}

export class App extends Component<{}, AppState> {
state = {
loadingAuth: true,
userInfo: null
};

unsubscribe: db.UnsubscribeCb;
componentWillMount() {
this.unsubscribe = db.active.subscribeAuthChange(userInfo => {
this.setState({ loadingAuth: false, userInfo });
});
}

componentWillUnmount() {
this.unsubscribe();
}

render() {
const { userInfo } = this.state;
return (
<div class="notebook">
<GlobalHeader subtitle="Notebook" subtitleLink="/">
<UserMenu userInfo={userInfo} />
</GlobalHeader>
<Router>
<RecentPage path="/" userInfo={userInfo} />
<NotebookPage path="/notebook/:nbId" userInfo={userInfo} />
<ProfilePage path="/user/:userId" userInfo={userInfo} />
<ErrorPage message="The page you're looking for doesn't exist." />
</Router>
</div>
);
}
}
1 change: 1 addition & 0 deletions src/components/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface UserMenuProps {

export function UserMenu(props): JSX.Element {
if (props.userInfo) {
// TODO "Your notebooks" link
return (
<div class="dropdown">
<Avatar size={32} userInfo={props.userInfo} />
Expand Down
15 changes: 6 additions & 9 deletions src/components/new_notebook_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,16 @@
*/

import { h } from "preact";
// TODO None of component in ./src/components should depend on ./src/db.ts.
import * as db from "../db";
import { nbUrl } from "./common";

export function newNotebookButton(): JSX.Element {
export interface NewNotebookProps {
onClick: () => void;
}

export function NewNotebook(props: NewNotebookProps): JSX.Element {
return (
<button
class="create-notebook"
onClick={async () => {
// Redirect to new notebook.
const nbId = await db.active.create();
window.location.href = nbUrl(nbId);
}}
onClick={ () => props.onClick && props.onClick()}
>
+ New Notebook
</button>
Expand Down
19 changes: 11 additions & 8 deletions src/components/notebook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface NotebookProps {
save?: (doc: types.NotebookDoc) => void;
initialDoc?: types.NotebookDoc;
userInfo?: types.UserInfo; // Info about currently logged in user.
clone?: () => void;
clone?: (doc: types.NotebookDoc) => void;
}

export interface NotebookState {
Expand Down Expand Up @@ -178,12 +178,7 @@ export class Notebook extends Component<NotebookProps, NotebookState> {
this.active = cellId;
}

onClone() {
if (this.props.clone) this.props.clone();
}

save() {
if (!this.props.save) return;
toDoc(): types.NotebookDoc {
const cells = [];
for (const key of this.state.order) {
cells.push(this.state.codes.get(key));
Expand All @@ -199,7 +194,15 @@ export class Notebook extends Component<NotebookProps, NotebookState> {
title: this.state.title,
updated: new Date()
};
this.props.save(doc);
return doc;
}

onClone() {
if (this.props.clone) this.props.clone(this.toDoc());
}

save() {
if (this.props.save) this.props.save(this.toDoc());
}

handleTitleChange(event) {
Expand Down
16 changes: 3 additions & 13 deletions src/components/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { Component, h } from "preact";
import * as db from "../db";
import * as types from "../types";
import { Avatar } from "./avatar";
import { docTitle, fmtDate } from "./common";
import { docTitle } from "./common";

export interface NotebookPreviewProps {
notebook: types.NbInfo;
Expand All @@ -40,23 +40,13 @@ export class NotebookPreview extends Component<NotebookPreviewProps, {}> {
notebook: { doc },
showTitle
} = this.props;
const { created, updated, title } = doc;
const { title } = doc;
const code = db.getInputCodes(doc).join("\n\n");
return (
<a onClick={this.onClick.bind(this)}>
<li>
<div class="code-snippit">{code}</div>
<div class="blurb">
{created ? (
<div class="date-created">
<p class="created">Created {fmtDate(created)}</p>
</div>
) : null}
{updated ? (
<div class="date-updated">
<p class="updated">Updated {fmtDate(updated)}</p>
</div>
) : null}
{!showTitle ? (
<div class="blurb-avatar">
<Avatar userInfo={doc.owner} />
Expand All @@ -65,7 +55,7 @@ export class NotebookPreview extends Component<NotebookPreviewProps, {}> {
{!showTitle ? (
<p class="blurb-name">{doc.owner.displayName}</p>
) : null}
{showTitle ? <p class="blurb-tile">{docTitle(title)}</p> : null}
{showTitle ? <p class="blurb-title">{docTitle(title)}</p> : null}
</div>
</li>
</a>
Expand Down
9 changes: 7 additions & 2 deletions src/components/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
import { Component, h } from "preact";
import * as types from "../types";
import { NotebookList } from "./list";
import { newNotebookButton } from "./new_notebook_button";
import { NewNotebook } from "./new_notebook_button";
import { UserTitle } from "./user_title";

export interface ProfileProps {
userInfo: types.UserInfo;
notebooks: types.NbInfo[];
onNewNotebookClicked?: () => void;
onClick?: (nbId: string) => void;
}

Expand All @@ -35,6 +36,10 @@ export class Profile extends Component<ProfileProps, {}> {
if (this.props.onClick) this.props.onClick(nbId);
}

onNewNotebookClicked() {
if (this.props.onNewNotebookClicked) this.props.onNewNotebookClicked();
}

render() {
if (this.props.notebooks.length === 0) {
return <h1>User has no notebooks.</h1>;
Expand All @@ -44,7 +49,7 @@ export class Profile extends Component<ProfileProps, {}> {
<div class="nb-listing">
<div class="nb-listing-header">
<UserTitle userInfo={doc.owner} />
{newNotebookButton()}
<NewNotebook onClick={this.onNewNotebookClicked.bind(this)} />
</div>
<NotebookList
showTitle={ true }
Expand Down
Loading

0 comments on commit 61f8610

Please sign in to comment.