Skip to content

Commit 43b0d1f

Browse files
qti3episcisaureus
authored andcommitted
Implement propel router
and some fixes in components
1 parent 2c8524d commit 43b0d1f

File tree

6 files changed

+232
-33
lines changed

6 files changed

+232
-33
lines changed

src/app.tsx

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*!
2+
Copyright 2018 Propel http://propel.site/. All rights reserved.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
import { Component, h } from "preact";
17+
import * as db from "./db";
18+
import { pushState, Router } from "./router";
19+
import * as types from "./types";
20+
import { equal } from "./util";
21+
22+
import { ErrorPage } from "./components/error";
23+
import { GlobalHeader } from "./components/header";
24+
import { Loading } from "./components/loading";
25+
import { UserMenu } from "./components/menu";
26+
import { Notebook } from "./components/notebook";
27+
import { Profile } from "./components/profile";
28+
import { Recent } from "./components/recent";
29+
30+
interface BindProps {
31+
[key: string]: (props: any) => Promise<any>;
32+
}
33+
34+
interface BindState {
35+
data: { [key: string]: string };
36+
error: string;
37+
}
38+
39+
/**
40+
* This react HOC can be used to bind result of some async
41+
* methods to props of the given component (C).
42+
* see: https://reactjs.org/docs/higher-order-components.html
43+
*
44+
* const newComponent = bind(Component, {
45+
* async prop(props) {
46+
* const re = await someAsyncActions();
47+
* return re;
48+
* }
49+
* });
50+
*/
51+
function bind(C, bindProps: BindProps) {
52+
return class extends Component<any, BindState> {
53+
state = { data: null, error: null };
54+
prevMatches = null;
55+
56+
async loadData() {
57+
if (equal(this.props.matches, this.prevMatches)) return;
58+
this.prevMatches = this.props.matches;
59+
const data = {};
60+
for (const key in bindProps) {
61+
if (!bindProps[key]) continue;
62+
try {
63+
data[key] = await bindProps[key](this.props);
64+
} catch (e) {
65+
this.setState({ data: null, error: e.message });
66+
return;
67+
}
68+
}
69+
this.setState({ data, error: null });
70+
}
71+
72+
render() {
73+
this.loadData();
74+
const { data, error } = this.state;
75+
if (error) return <ErrorPage message={error} />;
76+
if (!data) return <Loading />;
77+
return <C {...this.props} {...data} />;
78+
}
79+
};
80+
}
81+
82+
// An anonymous notebook doc for when users aren't logged in
83+
const anonDoc = {
84+
anonymous: true,
85+
cells: [],
86+
created: new Date(),
87+
owner: {
88+
displayName: "Anonymous",
89+
photoURL: require("./img/anon_profile.png"),
90+
uid: ""
91+
},
92+
title: "Anonymous Notebook",
93+
updated: new Date()
94+
};
95+
96+
// TODO Move these components to ./pages.tsx.
97+
// tslint:disable:variable-name
98+
async function onNewNotebookClicked() {
99+
const nbId = await db.active.create();
100+
// Redirect to new notebook.
101+
pushState(`/notebook/${nbId}`);
102+
}
103+
104+
async function onPreviewClicked(nbId: string) {
105+
// Redirect to notebook.
106+
pushState(`/notebook/${nbId}`);
107+
}
108+
109+
export const RecentPage = bind(Recent, {
110+
mostRecent() {
111+
return db.active.queryLatest();
112+
},
113+
async onClick() {
114+
return (nbId: string) => onPreviewClicked(nbId);
115+
},
116+
async onNewNotebookClicked() {
117+
return () => onNewNotebookClicked();
118+
}
119+
});
120+
121+
export const ProfilePage = bind(Profile, {
122+
notebooks(props) {
123+
const uid = props.matches.userId;
124+
return db.active.queryProfile(uid, 100);
125+
},
126+
async onClick() {
127+
return (nbId: string) => onPreviewClicked(nbId);
128+
},
129+
async onNewNotebookClicked() {
130+
return () => onNewNotebookClicked();
131+
}
132+
});
133+
134+
export const NotebookPage = bind(Notebook, {
135+
initialDoc(props) {
136+
const nbId = props.matches.nbId;
137+
return nbId === "anonymous"
138+
? Promise.resolve(anonDoc)
139+
: db.active.getDoc(nbId);
140+
},
141+
save(props) {
142+
const nbId = props.matches.nbId;
143+
const cb = async doc => {
144+
if (doc.anonymous) return;
145+
if (!props.userInfo) return;
146+
if (props.userInfo.uid !== doc.owner.uid) return;
147+
try {
148+
await db.active.updateDoc(nbId, doc);
149+
} catch (e) {
150+
// TODO
151+
console.log(e);
152+
}
153+
};
154+
return Promise.resolve(cb);
155+
},
156+
clone(props) {
157+
const cb = async doc => {
158+
const cloneId = await db.active.clone(doc);
159+
// Redirect to new notebook.
160+
pushState(`/notebook/${cloneId}`);
161+
};
162+
return Promise.resolve(cb);
163+
}
164+
});
165+
// tslint:enable:variable-name
166+
167+
export interface AppState {
168+
loadingAuth: boolean;
169+
userInfo: types.UserInfo;
170+
}
171+
172+
export class App extends Component<{}, AppState> {
173+
state = {
174+
loadingAuth: true,
175+
userInfo: null
176+
};
177+
178+
unsubscribe: db.UnsubscribeCb;
179+
componentWillMount() {
180+
this.unsubscribe = db.active.subscribeAuthChange(userInfo => {
181+
this.setState({ loadingAuth: false, userInfo });
182+
});
183+
}
184+
185+
componentWillUnmount() {
186+
this.unsubscribe();
187+
}
188+
189+
render() {
190+
const { userInfo } = this.state;
191+
return (
192+
<div class="notebook">
193+
<GlobalHeader subtitle="Notebook" subtitleLink="/">
194+
<UserMenu userInfo={userInfo} />
195+
</GlobalHeader>
196+
<Router>
197+
<RecentPage path="/" userInfo={userInfo} />
198+
<NotebookPage path="/notebook/:nbId" userInfo={userInfo} />
199+
<ProfilePage path="/user/:userId" userInfo={userInfo} />
200+
<ErrorPage message="The page you're looking for doesn't exist." />
201+
</Router>
202+
</div>
203+
);
204+
}
205+
}

src/components/menu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface UserMenuProps {
2626

2727
export function UserMenu(props): JSX.Element {
2828
if (props.userInfo) {
29+
// TODO "Your notebooks" link
2930
return (
3031
<div class="dropdown">
3132
<Avatar size={32} userInfo={props.userInfo} />

src/components/new_notebook_button.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,16 @@
1414
*/
1515

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

21-
export function newNotebookButton(): JSX.Element {
18+
export interface NewNotebookProps {
19+
onClick: () => void;
20+
}
21+
22+
export function NewNotebook(props: NewNotebookProps): JSX.Element {
2223
return (
2324
<button
2425
class="create-notebook"
25-
onClick={async () => {
26-
// Redirect to new notebook.
27-
const nbId = await db.active.create();
28-
window.location.href = nbUrl(nbId);
29-
}}
26+
onClick={ () => props.onClick && props.onClick()}
3027
>
3128
+ New Notebook
3229
</button>

src/components/notebook.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export interface NotebookProps {
3535
save?: (doc: types.NotebookDoc) => void;
3636
initialDoc?: types.NotebookDoc;
3737
userInfo?: types.UserInfo; // Info about currently logged in user.
38-
clone?: () => void;
38+
clone?: (doc: types.NotebookDoc) => void;
3939
}
4040

4141
export interface NotebookState {
@@ -178,12 +178,7 @@ export class Notebook extends Component<NotebookProps, NotebookState> {
178178
this.active = cellId;
179179
}
180180

181-
onClone() {
182-
if (this.props.clone) this.props.clone();
183-
}
184-
185-
save() {
186-
if (!this.props.save) return;
181+
toDoc(): types.NotebookDoc {
187182
const cells = [];
188183
for (const key of this.state.order) {
189184
cells.push(this.state.codes.get(key));
@@ -199,7 +194,15 @@ export class Notebook extends Component<NotebookProps, NotebookState> {
199194
title: this.state.title,
200195
updated: new Date()
201196
};
202-
this.props.save(doc);
197+
return doc;
198+
}
199+
200+
onClone() {
201+
if (this.props.clone) this.props.clone(this.toDoc());
202+
}
203+
204+
save() {
205+
if (this.props.save) this.props.save(this.toDoc());
203206
}
204207

205208
handleTitleChange(event) {

src/components/user_title.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,20 @@ import { h } from "preact";
1717
import * as types from "../types";
1818
import { Avatar } from "./avatar";
1919

20-
export function profileLink(
21-
u: types.UserInfo,
22-
text: string = null
23-
): JSX.Element {
24-
const href = window.location.origin + "/notebook?profile=" + u.uid;
25-
return (
26-
<a class="profile-link" href={href}>
27-
{text ? text : u.displayName}
28-
</a>
29-
);
30-
}
31-
3220
export interface UserTitleProps {
3321
userInfo: types.UserInfo;
3422
}
3523

3624
export function UserTitle(props: UserTitleProps): JSX.Element {
25+
const href = window.location.origin + "/user/" + props.userInfo.uid;
3726
return (
38-
<div class="most-recent-header-title">
27+
<div class="nb-listing-header-title">
3928
<Avatar userInfo={props.userInfo} />
40-
<h2>{profileLink(props.userInfo)}</h2>
29+
<h2>
30+
<a class="profile-link" href={href} >
31+
{props.userInfo.displayName}
32+
</a>
33+
</h2>
4134
</div>
4235
);
4336
}

src/main.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { h, render, rerender } from "preact";
2+
import { App } from "./app";
23
import { drainExecuteQueue } from "./components/cell";
34
import { enableFirebase } from "./db";
4-
import { Router } from "./pages";
55
import { assert, IS_WEB } from "./util";
66

77
assert(IS_WEB);
88

99
enableFirebase();
1010

1111
window.addEventListener("load", async () => {
12-
render(<Router />, document.body, document.body.children[0]);
12+
render(<App />, document.body, document.body.children[0]);
1313
await drainExecuteQueue();
1414

1515
// If we're in a testing environment...

0 commit comments

Comments
 (0)