-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
9 changed files
with
252 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.