create-wirejs-app 2.0.9 → 2.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.js +1 -5
- package/package.json +1 -1
- package/templates/default/api/index.ts +63 -0
- package/templates/default/gitignore +2 -0
- package/templates/default/index.ts +63 -0
- package/templates/default/package.json +5 -4
- package/templates/default/src/components/{account-menu.js → account-menu.ts} +29 -33
- package/templates/default/src/components/{authenticator.js → authenticator.ts} +40 -56
- package/templates/default/src/ssg/{index.js → index.ts} +1 -0
- package/templates/default/src/ssg/{todo-app.js → todo-app.ts} +11 -10
- package/templates/default/src/ssr/simple-wiki/%.ts +121 -0
- package/templates/default/tsconfig.json +15 -0
- package/templates/default/api/index.js +0 -99
- package/templates/default/src/build_id.json +0 -1
- package/templates/default/src/components/countdown.js +0 -36
- package/templates/default/src/layouts/bare.html +0 -1
- package/templates/default/src/layouts/core.css +0 -3
- package/templates/default/src/layouts/default.css +0 -222
- package/templates/default/src/layouts/default.html +0 -41
- package/templates/default/src/layouts/default.js +0 -3
- package/templates/default/src/lib/sample-lib.js +0 -8
- package/templates/default/src/ssr/simple-wiki/STAR.js +0 -101
package/bin.js
CHANGED
|
@@ -18,11 +18,7 @@ const [
|
|
|
18
18
|
await copy(`${__dirname}/templates/default`, `./${projectName}`);
|
|
19
19
|
await copy(`${__dirname}/templates/default/gitignore`, `./${projectName}/.gitignore`);
|
|
20
20
|
await fs.promises.unlink(`./${projectName}/gitignore`);
|
|
21
|
-
|
|
22
|
-
`./${projectName}/src/ssr/simple-wiki/STAR.js`,
|
|
23
|
-
`./${projectName}/src/ssr/simple-wiki/*.js`
|
|
24
|
-
)
|
|
25
|
-
|
|
21
|
+
|
|
26
22
|
const packageJson = await fs.readFileSync(`./${projectName}/package.json`);
|
|
27
23
|
fs.writeFileSync(
|
|
28
24
|
`./${projectName}/package.json`,
|
package/package.json
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AuthenticationService, FileService, withContext } from 'wirejs-resources';
|
|
2
|
+
|
|
3
|
+
const userTodos = new FileService('app', 'userTodoApp');
|
|
4
|
+
const wikiPages = new FileService('app', 'wikiPages');
|
|
5
|
+
const authService = new AuthenticationService('app', 'core-users');
|
|
6
|
+
|
|
7
|
+
export const auth = authService.buildApi();
|
|
8
|
+
|
|
9
|
+
export type Todo = {
|
|
10
|
+
id: string;
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const todos = withContext(context => ({
|
|
15
|
+
async read(): Promise<Todo[]> {
|
|
16
|
+
const user = await auth.requireCurrentUser(context);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const todos = await userTodos.read(`${user.id}/todos.json`);
|
|
20
|
+
return todos ? JSON.parse(todos) : [];
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
async write(todos: Todo[]) {
|
|
26
|
+
const user = await auth.requireCurrentUser(context);
|
|
27
|
+
|
|
28
|
+
if (!Array.isArray(todos) || !todos.every(todo =>
|
|
29
|
+
typeof todo.id === 'string'
|
|
30
|
+
&& typeof todo.text === 'string')
|
|
31
|
+
) {
|
|
32
|
+
throw new Error("Invalid todos!");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const finalTodos = todos.map(todo => ({ id: todo.id, text: todo.text }));
|
|
36
|
+
await userTodos.write(`${user.id}/todos.json`, JSON.stringify(finalTodos));
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
function normalizeWikiPageFilename(page: string) {
|
|
43
|
+
return page.replace(/[^-_a-zA-Z0-9/]/g, '-') + '.md';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const wiki = withContext(context => ({
|
|
47
|
+
async read(page: string) {
|
|
48
|
+
const filename = normalizeWikiPageFilename(page);
|
|
49
|
+
try {
|
|
50
|
+
return await wikiPages.read(filename);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async write(page: string, content: string) {
|
|
56
|
+
await auth.requireCurrentUser(context);
|
|
57
|
+
|
|
58
|
+
const filename = normalizeWikiPageFilename(page);
|
|
59
|
+
await wikiPages.write(filename, content);
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AuthenticationService, FileService, withContext } from 'wirejs-resources';
|
|
2
|
+
|
|
3
|
+
const userTodos = new FileService('app', 'userTodoApp');
|
|
4
|
+
const wikiPages = new FileService('app', 'wikiPages');
|
|
5
|
+
const authService = new AuthenticationService('app', 'core-users');
|
|
6
|
+
|
|
7
|
+
export const auth = authService.buildApi();
|
|
8
|
+
|
|
9
|
+
export type Todo = {
|
|
10
|
+
id: string;
|
|
11
|
+
text: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const todos = withContext(context => ({
|
|
15
|
+
async read(): Promise<Todo[]> {
|
|
16
|
+
const user = await auth.requireCurrentUser(context);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const todos = await userTodos.read(`${user.id}/todos.json`);
|
|
20
|
+
return todos ? JSON.parse(todos) : [];
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
async write(todos: Todo[]) {
|
|
26
|
+
const user = await auth.requireCurrentUser(context);
|
|
27
|
+
|
|
28
|
+
if (!Array.isArray(todos) || !todos.every(todo =>
|
|
29
|
+
typeof todo.id === 'string'
|
|
30
|
+
&& typeof todo.text === 'string')
|
|
31
|
+
) {
|
|
32
|
+
throw new Error("Invalid todos!");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const finalTodos = todos.map(todo => ({ id: todo.id, text: todo.text }));
|
|
36
|
+
await userTodos.write(`${user.id}/todos.json`, JSON.stringify(finalTodos));
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
function normalizeWikiPageFilename(page: string) {
|
|
43
|
+
return page.replace(/[^-_a-zA-Z0-9/]/g, '-') + '.md';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const wiki = withContext(context => ({
|
|
47
|
+
async read(page: string) {
|
|
48
|
+
const filename = normalizeWikiPageFilename(page);
|
|
49
|
+
try {
|
|
50
|
+
return await wikiPages.read(filename);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
async write(page: string, content: string) {
|
|
56
|
+
await auth.requireCurrentUser(context);
|
|
57
|
+
|
|
58
|
+
const filename = normalizeWikiPageFilename(page);
|
|
59
|
+
await wikiPages.write(filename, content);
|
|
60
|
+
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
"api"
|
|
9
9
|
],
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"wirejs-dom": "^1.0.35",
|
|
12
|
-
"wirejs-resources": "^0.1.9-alpha",
|
|
13
11
|
"dompurify": "^3.2.3",
|
|
14
|
-
"marked": "^15.0.6"
|
|
12
|
+
"marked": "^15.0.6",
|
|
13
|
+
"wirejs-dom": "^1.0.38",
|
|
14
|
+
"wirejs-resources": "^0.1.9-alpha"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"wirejs-scripts": "^3.0.
|
|
17
|
+
"wirejs-scripts": "^3.0.8",
|
|
18
|
+
"typescript": "^5.7.3"
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
20
21
|
"prebuild": "npm run prebuild --workspaces --if-present",
|
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import { attribute, html, node, text, id } from 'wirejs-dom/v2';
|
|
2
2
|
import { authenticator } from './authenticator.js';
|
|
3
|
+
import type {
|
|
4
|
+
AuthenticationApi,
|
|
5
|
+
AuthenticationMachineState,
|
|
6
|
+
} from 'wirejs-resources';
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
* @typedef {import('wirejs-services').AuthenticationService} AuthenticationService
|
|
6
|
-
* @typedef {ReturnType<AuthenticationService['buildApi']>} AuthStateApi
|
|
7
|
-
* @typedef {Awaited<ReturnType<AuthStateApi['getState']>>} AuthState
|
|
8
|
-
* @typedef {AuthState['actions'][string]} AuthStateAction
|
|
9
|
-
* @typedef {Parameters<AuthStateApi['setState']>[0]} AuthStateActionInput
|
|
10
|
-
* @typedef {import('wirejs-services').Context} Context
|
|
11
|
-
*/
|
|
8
|
+
type Callback = (state: AuthenticationMachineState) => any;
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
export const accountMenu = ({ api, initialState }: {
|
|
11
|
+
api: AuthenticationApi,
|
|
12
|
+
initialState?: AuthenticationMachineState
|
|
13
|
+
}) => {
|
|
17
14
|
const uiState = {
|
|
18
15
|
expanded: false
|
|
19
16
|
};
|
|
20
17
|
|
|
21
|
-
|
|
22
|
-
* @type {Set<(state: AuthState) => any>}
|
|
23
|
-
*/
|
|
24
|
-
const listeners = new Set();
|
|
18
|
+
const listeners = new Set<Callback>();
|
|
25
19
|
|
|
26
|
-
const listenForClose =
|
|
20
|
+
const listenForClose = (
|
|
21
|
+
event: (MouseEvent | KeyboardEvent)
|
|
22
|
+
) => {
|
|
27
23
|
if (
|
|
28
|
-
(event.type === 'click' && !self.data.menu.contains(event.target))
|
|
29
|
-
|| (event.type === 'keyup' && event.key === 'Escape')
|
|
24
|
+
(event.type === 'click' && !self.data.menu.contains(event.target as any))
|
|
25
|
+
|| (event.type === 'keyup' && (event as any).key === 'Escape')
|
|
30
26
|
) {
|
|
31
27
|
close()
|
|
32
28
|
}
|
|
@@ -51,9 +47,9 @@ export const accountMenu = (api) => {
|
|
|
51
47
|
self.data.menu.style.right = `${document.body.clientWidth - position.right + 16}px`;
|
|
52
48
|
};
|
|
53
49
|
|
|
54
|
-
const authenticatorNode = authenticator(api);
|
|
50
|
+
const authenticatorNode = authenticator(api, initialState);
|
|
55
51
|
authenticatorNode.data.onchange(state => {
|
|
56
|
-
self.data.user = state.
|
|
52
|
+
self.data.user = state.user?.username || '';
|
|
57
53
|
close();
|
|
58
54
|
for (const listener of listeners) {
|
|
59
55
|
try {
|
|
@@ -67,7 +63,10 @@ export const accountMenu = (api) => {
|
|
|
67
63
|
const self = html`<accountmenu style='display: inline-block;'>
|
|
68
64
|
<div
|
|
69
65
|
style='display: inline-block;'
|
|
70
|
-
>${node(
|
|
66
|
+
>${node(
|
|
67
|
+
'user',
|
|
68
|
+
initialState?.user?.username || '',
|
|
69
|
+
name => name ? html`<b>${name}</b>` : html`<i>Anonymous</i>`)}</div>
|
|
71
70
|
<div style='
|
|
72
71
|
display: inline-block;
|
|
73
72
|
border: 1px solid silver;
|
|
@@ -89,7 +88,7 @@ export const accountMenu = (api) => {
|
|
|
89
88
|
}
|
|
90
89
|
}}
|
|
91
90
|
>☰</div>
|
|
92
|
-
<div ${id('menu')} style='
|
|
91
|
+
<div ${id('menu', HTMLDivElement)} style='
|
|
93
92
|
display: none;
|
|
94
93
|
position: absolute;
|
|
95
94
|
border: 1px solid gray;
|
|
@@ -99,21 +98,18 @@ export const accountMenu = (api) => {
|
|
|
99
98
|
box-shadow: -0.125rem 0.125rem 0.25rem lightgray;
|
|
100
99
|
'>${node('authenticator', authenticatorNode)}</div>
|
|
101
100
|
</accountmenu>`.onadd(async self => {
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
if (!initialState) {
|
|
102
|
+
const state = await api.getState(null);
|
|
103
|
+
authenticatorNode.data.setState(state);
|
|
104
|
+
self.data.user = state.user?.username || '';
|
|
105
|
+
}
|
|
104
106
|
}).extend(self => ({
|
|
105
107
|
data: {
|
|
106
|
-
|
|
107
|
-
* @param {(state: AuthState) => any} callback
|
|
108
|
-
*/
|
|
109
|
-
onchange: (callback) => {
|
|
108
|
+
onchange: (callback: Callback) => {
|
|
110
109
|
listeners.add(callback);
|
|
111
110
|
},
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
* @param {(state: AuthState) => any} callback
|
|
115
|
-
*/
|
|
116
|
-
removeonchange: (callback) => {
|
|
112
|
+
removeonchange: (callback: Callback) => {
|
|
117
113
|
listeners.delete(callback);
|
|
118
114
|
},
|
|
119
115
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { attribute, html, node } from 'wirejs-dom/v2';
|
|
2
|
+
import type {
|
|
3
|
+
AuthenticationApi,
|
|
4
|
+
AuthenticationState,
|
|
5
|
+
AuthenticationMachineState,
|
|
6
|
+
AuthenticationMachineAction,
|
|
7
|
+
AuthenticationMachineInput,
|
|
8
|
+
} from 'wirejs-resources';
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
* @typedef {import('wirejs-services').AuthenticationService} AuthenticationService
|
|
5
|
-
* @typedef {ReturnType<AuthenticationService['buildApi']>} AuthStateApi
|
|
6
|
-
* @typedef {Awaited<ReturnType<AuthStateApi['getState']>>} AuthState
|
|
7
|
-
* @typedef {AuthState['actions'][string]} AuthStateAction
|
|
8
|
-
* @typedef {Parameters<AuthStateApi['setState']>[0]} AuthStateActionInput
|
|
9
|
-
*/
|
|
10
|
+
type AuthCallback = (state: AuthenticationMachineState) => any;
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const inputs = Object.entries(action.inputs || []).map(([name, { label, type }]) => {
|
|
12
|
+
export const authenticatoraction = (
|
|
13
|
+
action: AuthenticationMachineAction,
|
|
14
|
+
act: (input: AuthenticationMachineInput) => void
|
|
15
|
+
) => {
|
|
16
|
+
const inputs = Object.entries(action.fields || []).map(([name, { label, type }]) => {
|
|
17
17
|
const id = `input_${Math.floor(Math.random() * 1_000_000)}`;
|
|
18
18
|
const input = html`<div>
|
|
19
19
|
<label for=${id}>${label}</label>
|
|
@@ -44,16 +44,16 @@ export const authenticatoraction = (action, act) => {
|
|
|
44
44
|
|
|
45
45
|
const actors = link ?? buttons;
|
|
46
46
|
|
|
47
|
-
if (action.
|
|
47
|
+
if (action.fields && Object.keys(action.fields).length > 0) {
|
|
48
48
|
return html`<authenticatoraction>
|
|
49
49
|
<div>
|
|
50
50
|
<h4 style='margin-top: 1rem; margin-bottom: 0.5rem;'>${action.name}</h4>
|
|
51
51
|
<form
|
|
52
|
-
onsubmit=${evt => {
|
|
52
|
+
onsubmit=${(evt: SubmitEvent) => {
|
|
53
53
|
evt.preventDefault();
|
|
54
54
|
act({
|
|
55
55
|
key: action.key,
|
|
56
|
-
verb: evt.submitter?.value,
|
|
56
|
+
verb: (evt.submitter as any)?.value,
|
|
57
57
|
inputs: Object.fromEntries(inputs.map(input => ([
|
|
58
58
|
input.data.name,
|
|
59
59
|
input.data.value
|
|
@@ -74,71 +74,55 @@ export const authenticatoraction = (action, act) => {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* @type {Set<(state: AuthState) => any>}
|
|
84
|
-
*/
|
|
85
|
-
const listeners = new Set();
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* @type {AuthState}
|
|
89
|
-
*/
|
|
90
|
-
let lastKnownState = undefined;
|
|
77
|
+
export const authenticator = (
|
|
78
|
+
stateManager: AuthenticationApi,
|
|
79
|
+
initialState?: AuthenticationMachineState
|
|
80
|
+
) => {
|
|
81
|
+
const listeners = new Set<AuthCallback>();
|
|
82
|
+
let lastKnownState: AuthenticationMachineState | undefined = undefined;
|
|
91
83
|
|
|
92
84
|
const self = html`<authenticator style='display: block; min-width: 15em;'>
|
|
93
|
-
${node('state', html`<span>Loading ...</span>`)}
|
|
94
|
-
</authenticator>`.extend(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
renderState(state) {
|
|
99
|
-
lastKnownState = state;
|
|
100
|
-
if (state.errors) {
|
|
101
|
-
alert(state.errors.map(e => e.message).join("\n\n"));
|
|
85
|
+
${node('state', html`<span>Loading ...</span>` as HTMLElement)}
|
|
86
|
+
</authenticator>`.extend(() => ({
|
|
87
|
+
renderState(state: AuthenticationMachineState | { errors: any[] }) {
|
|
88
|
+
if ('errors' in state && state.errors) {
|
|
89
|
+
alert((state as any).errors.map((e: any) => e.message).join("\n\n"));
|
|
102
90
|
} else {
|
|
91
|
+
lastKnownState = state as AuthenticationMachineState;
|
|
103
92
|
self.data.state = html`<div>
|
|
104
|
-
<div>${
|
|
105
|
-
<div>${Object.entries(
|
|
106
|
-
return authenticatoraction({
|
|
107
|
-
self.renderState(await stateManager.setState(
|
|
93
|
+
<div>${lastKnownState.message || ''}</div>
|
|
94
|
+
<div>${Object.entries(lastKnownState.actions).map(([_key, action]) => {
|
|
95
|
+
return authenticatoraction({...action}, async act => {
|
|
96
|
+
self.renderState(await stateManager.setState(null, act));
|
|
108
97
|
});
|
|
109
98
|
})}</div>
|
|
110
99
|
</div>`;
|
|
111
100
|
}
|
|
112
101
|
for (const listener of listeners) {
|
|
113
102
|
try {
|
|
114
|
-
listener(state);
|
|
103
|
+
listener(state as AuthenticationMachineState);
|
|
115
104
|
} catch (error) {
|
|
116
105
|
console.error("Error calling auth state listener.");
|
|
117
106
|
}
|
|
118
107
|
}
|
|
119
108
|
}
|
|
120
109
|
})).onadd(async (self) => {
|
|
121
|
-
|
|
110
|
+
if (initialState) {
|
|
111
|
+
console.log('authenticator render state');
|
|
112
|
+
self.renderState(initialState)
|
|
113
|
+
}
|
|
122
114
|
}).extend(self => ({
|
|
123
115
|
data: {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*/
|
|
127
|
-
onchange: (callback) => {
|
|
116
|
+
setState: (state: AuthenticationMachineState) => self.renderState(state),
|
|
117
|
+
onchange: (callback: AuthCallback) => {
|
|
128
118
|
listeners.add(callback);
|
|
129
119
|
},
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* @param {(state: AuthState) => any} callback
|
|
133
|
-
*/
|
|
134
|
-
removeonchange: (callback) => {
|
|
120
|
+
removeonchange: (callback: AuthCallback) => {
|
|
135
121
|
listeners.delete(callback);
|
|
136
122
|
},
|
|
137
|
-
|
|
138
123
|
focus: () => {
|
|
139
124
|
[...self.getElementsByTagName('input')].shift()?.focus();
|
|
140
125
|
},
|
|
141
|
-
|
|
142
126
|
get lastKnownState() {
|
|
143
127
|
return lastKnownState;
|
|
144
128
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { html, node, list, attribute, hydrate } from 'wirejs-dom/v2';
|
|
2
2
|
import { accountMenu } from '../components/account-menu.js';
|
|
3
|
-
import { auth, todos } from 'my-api';
|
|
3
|
+
import { auth, todos, Todo } from 'my-api';
|
|
4
4
|
|
|
5
5
|
function Todos() {
|
|
6
6
|
const save = async () => {
|
|
7
7
|
try {
|
|
8
|
-
await todos.write(
|
|
8
|
+
await todos.write(null, self.data.todos);
|
|
9
9
|
} catch (error) {
|
|
10
10
|
alert(error);
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const remove = todo => {
|
|
14
|
+
const remove = (todo: Todo) => {
|
|
15
15
|
self.data.todos = self.data.todos.filter(t => t.id !== todo.id);
|
|
16
16
|
save();
|
|
17
17
|
}
|
|
@@ -20,34 +20,34 @@ function Todos() {
|
|
|
20
20
|
|
|
21
21
|
const self = html`<div>
|
|
22
22
|
<h4>Your Todos</h4>
|
|
23
|
-
<ol>${list('todos', todo => html`<li>
|
|
23
|
+
<ol>${list('todos', (todo: Todo) => html`<li>
|
|
24
24
|
${todo.text} : <span
|
|
25
25
|
style='color: darkred; font-weight: bold; cursor: pointer;'
|
|
26
26
|
onclick=${() => remove(todo)}
|
|
27
27
|
>X</span>
|
|
28
28
|
</li>`)}</ol>
|
|
29
29
|
<div>
|
|
30
|
-
<form onsubmit=${event => {
|
|
30
|
+
<form onsubmit=${(event: Event) => {
|
|
31
31
|
event.preventDefault();
|
|
32
32
|
self.data.todos.push({ id: newid(), text: self.data.newTodo });
|
|
33
33
|
self.data.newTodo = '';
|
|
34
34
|
save();
|
|
35
35
|
}}>
|
|
36
|
-
<input type='text' value=${attribute('newTodo', '')} />
|
|
36
|
+
<input type='text' value=${attribute('newTodo', '' as string)} />
|
|
37
37
|
<input type='submit' value='Add' />
|
|
38
38
|
</form>
|
|
39
39
|
</div>
|
|
40
40
|
<div>`.onadd(async self => {
|
|
41
|
-
self.data.todos = await todos.read(
|
|
41
|
+
self.data.todos = await todos.read(null);
|
|
42
42
|
});
|
|
43
43
|
return self;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async function App() {
|
|
47
|
-
const accountMenuNode = accountMenu(auth);
|
|
47
|
+
const accountMenuNode = accountMenu({ api: auth });
|
|
48
48
|
|
|
49
49
|
accountMenuNode.data.onchange(async state => {
|
|
50
|
-
if (state.state
|
|
50
|
+
if (state.state === 'authenticated') {
|
|
51
51
|
self.data.content = Todos();
|
|
52
52
|
} else {
|
|
53
53
|
self.data.content = html`<div>You need to sign in to add your todo list.</div>`;
|
|
@@ -67,6 +67,7 @@ export async function generate() {
|
|
|
67
67
|
<!doctype html>
|
|
68
68
|
<html>
|
|
69
69
|
<head>
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
70
71
|
<title>Todo App</title>
|
|
71
72
|
</head>
|
|
72
73
|
<body>
|
|
@@ -80,4 +81,4 @@ export async function generate() {
|
|
|
80
81
|
return page;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
hydrate('app', App);
|
|
84
|
+
hydrate('app', App as any);
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { marked } from 'marked';
|
|
2
|
+
import DOMPurify from 'dompurify';
|
|
3
|
+
import { html, id, text, hydrate, node, list, attribute, KindaPretty } from 'wirejs-dom/v2';
|
|
4
|
+
import type { AuthenticationMachineState, Context } from 'wirejs-resources';
|
|
5
|
+
import { accountMenu } from '../../components/account-menu.js';
|
|
6
|
+
import { auth, wiki } from 'my-api';
|
|
7
|
+
|
|
8
|
+
type WithoutNodes<T> = KindaPretty<{
|
|
9
|
+
[K in keyof T]: T[K] extends Node
|
|
10
|
+
? T[K] extends { data: any } ? WithoutNodes<T[K]['data']> : undefined
|
|
11
|
+
: WithoutNodes<T[K]>
|
|
12
|
+
}>
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Shallow check for a `data` hydration property. If present, returns the
|
|
16
|
+
* argument typed according to the given `T`.
|
|
17
|
+
*/
|
|
18
|
+
function initData<T extends { data: any }>(arg0: any): WithoutNodes<T['data']> | undefined {
|
|
19
|
+
return arg0?.data ? arg0.data : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function Wiki(init: { context?: Context, data?: any }) {
|
|
23
|
+
const { context } = init;
|
|
24
|
+
|
|
25
|
+
const data = initData<typeof self>(init);
|
|
26
|
+
|
|
27
|
+
console.log('Wiki init', init);
|
|
28
|
+
const filepath = (context || window).location.pathname;
|
|
29
|
+
|
|
30
|
+
const content = data?.content || await wiki.read(context, filepath);
|
|
31
|
+
const initialState: AuthenticationMachineState =
|
|
32
|
+
data?.initialAuthState || await auth.getState(context)
|
|
33
|
+
;
|
|
34
|
+
const accountMenuNode = accountMenu({ api: auth, initialState });
|
|
35
|
+
|
|
36
|
+
let markdown: string = content ?? `This page doesn't exist yet`;
|
|
37
|
+
const signedOutAction = html`<i>(<b>Sign in</b> to edit.)</i>`;
|
|
38
|
+
const signedInAction = html`<button onclick=${enableEditing}>edit</button>`;
|
|
39
|
+
const invisibleDiv = html`<div style='display: none;'></div>`;
|
|
40
|
+
const editor = html`<div>
|
|
41
|
+
<textarea
|
|
42
|
+
${id('textarea', HTMLTextAreaElement)}
|
|
43
|
+
style='width: 20em; height: 10em;'
|
|
44
|
+
></textarea>
|
|
45
|
+
</div>`;
|
|
46
|
+
|
|
47
|
+
accountMenuNode.data.onchange(state => {
|
|
48
|
+
self.data.actions = actionsFor(state);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function actionsFor(state: AuthenticationMachineState) {
|
|
52
|
+
return state.state === 'authenticated' ? signedInAction : signedOutAction;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function enableEditing() {
|
|
56
|
+
editor.data.textarea.value = markdown;
|
|
57
|
+
self.data.editor = editor;
|
|
58
|
+
self.data.actions = html`<div>
|
|
59
|
+
<button onclick=${submitChanges}>save</button>
|
|
60
|
+
<button onclick=${cancelChanges}>cancel</button>
|
|
61
|
+
</div>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function submitChanges() {
|
|
65
|
+
markdown = editor.data.textarea.value;
|
|
66
|
+
await wiki.write(context, filepath, markdown);
|
|
67
|
+
self.data.content = markdown;
|
|
68
|
+
self.data.actions = signedInAction;
|
|
69
|
+
self.data.editor = invisibleDiv;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cancelChanges() {
|
|
73
|
+
self.data.actions = signedInAction;
|
|
74
|
+
self.data.editor = html`<div style='display: none;'></div>`;
|
|
75
|
+
self.data.content = markdown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const self = html`<div id='wiki'>
|
|
79
|
+
<div style='float: right;'>${accountMenuNode}</div>
|
|
80
|
+
${node('content', markdown, md =>
|
|
81
|
+
html`<div>${DOMPurify.sanitize(marked.parse(md!) as string)}</div>`)
|
|
82
|
+
}
|
|
83
|
+
${node('editor', invisibleDiv)}
|
|
84
|
+
${node('actions', actionsFor(initialState))}
|
|
85
|
+
</div>`.extend(self => ({
|
|
86
|
+
data: {
|
|
87
|
+
initialAuthState: initialState
|
|
88
|
+
}
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
return self;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function generate(context: Context) {
|
|
95
|
+
const visiblePath = context.location.pathname
|
|
96
|
+
.replaceAll('/', ' > ')
|
|
97
|
+
.replaceAll('<', '<')
|
|
98
|
+
.replaceAll('>', '>')
|
|
99
|
+
.replaceAll('-', ' ')
|
|
100
|
+
.replace(/\s+/g, ' ')
|
|
101
|
+
;
|
|
102
|
+
|
|
103
|
+
const page = html`
|
|
104
|
+
<!doctype html>
|
|
105
|
+
<html>
|
|
106
|
+
<head>
|
|
107
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
108
|
+
<title>Wiki ${visiblePath}</title>
|
|
109
|
+
</head>
|
|
110
|
+
<body>
|
|
111
|
+
<p><a href='/'>Home</a></p>
|
|
112
|
+
<h1>Wiki ${visiblePath}</h1>
|
|
113
|
+
${await Wiki({ context })}
|
|
114
|
+
</body>
|
|
115
|
+
</html>
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
return page;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
hydrate('wiki', Wiki);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "ESNext",
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noImplicitAny": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules"]
|
|
15
|
+
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { AuthenticationService, FileService, withContext } from 'wirejs-resources';
|
|
2
|
-
import { defaultGreeting } from '../src/lib/sample-lib.js';
|
|
3
|
-
|
|
4
|
-
const userTodos = new FileService('app', 'userTodoApp');
|
|
5
|
-
const wikiPages = new FileService('app', 'wikiPages');
|
|
6
|
-
const authService = new AuthenticationService('app', 'core-users');
|
|
7
|
-
|
|
8
|
-
export const auth = authService.buildApi();
|
|
9
|
-
|
|
10
|
-
async function currentUser(context) {
|
|
11
|
-
const { user } = await authService.getBaseState(context.cookies);
|
|
12
|
-
return user;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Given a name, this will return a friendly, personalized greeting.
|
|
17
|
-
* @param {string} name
|
|
18
|
-
* @returns {Promise<string>} A friendly greeting.
|
|
19
|
-
*/
|
|
20
|
-
export const hello = withContext(context => async (name) => {
|
|
21
|
-
const user = await currentUser();
|
|
22
|
-
return `${defaultGreeting()}, ${user ? `<b>${user}</b>` : '<i>Anonymous</i>'}.`;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export const todos = withContext(context => ({
|
|
26
|
-
async read() {
|
|
27
|
-
const user = await currentUser(context);
|
|
28
|
-
|
|
29
|
-
console.log('current user', user);
|
|
30
|
-
|
|
31
|
-
if (!user) {
|
|
32
|
-
throw new Error("Unauthorized");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const todos = await userTodos.read(`${user}/todos.json`);
|
|
37
|
-
return todos ? JSON.parse(todos) : [];
|
|
38
|
-
} catch (error) {
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
/**
|
|
43
|
-
* @param {string[]} todos
|
|
44
|
-
*/
|
|
45
|
-
async write(todos) {
|
|
46
|
-
const user = await currentUser(context);
|
|
47
|
-
|
|
48
|
-
if (!user) {
|
|
49
|
-
throw new Error("Unauthorized");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!Array.isArray(todos)) {
|
|
53
|
-
throw new Error("Invalid todos!");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (!todos.every(todo =>
|
|
57
|
-
typeof todo.id === 'string'
|
|
58
|
-
&& typeof todo.text === 'string')
|
|
59
|
-
) {
|
|
60
|
-
throw new Error("Invalid todos!");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const finalTodos = todos.map(todo => ({ id: todo.id, text: todo.text }));
|
|
64
|
-
await userTodos.write(`${user}/todos.json`, JSON.stringify(finalTodos));
|
|
65
|
-
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
function normalizeWikiPageFilename(page) {
|
|
71
|
-
return page.replace(/[^-_a-zA-Z0-9/]/g, '-') + '.md';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const wiki = withContext(context => ({
|
|
75
|
-
async read(page) {
|
|
76
|
-
const filename = normalizeWikiPageFilename(page);
|
|
77
|
-
try {
|
|
78
|
-
return await wikiPages.read(filename);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
console.log("returning empty content");
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
/**
|
|
85
|
-
* @param {string[]} todos
|
|
86
|
-
*/
|
|
87
|
-
async write(page, content) {
|
|
88
|
-
const user = await currentUser(context);
|
|
89
|
-
|
|
90
|
-
if (!user) {
|
|
91
|
-
throw new Error("Unauthorized");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const filename = normalizeWikiPageFilename(page);
|
|
95
|
-
await wikiPages.write(filename, content);
|
|
96
|
-
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"1731789421822"
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { hello } from 'my-api';
|
|
2
|
-
import { html, text, node } from 'wirejs-dom/v2';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Counts down from a given time.
|
|
6
|
-
*
|
|
7
|
-
* @param {number} from - The time to count down "from".
|
|
8
|
-
* @returns
|
|
9
|
-
*/
|
|
10
|
-
export async function Countdown(T = 10) {
|
|
11
|
-
return html`<div>
|
|
12
|
-
${node('remaining', T, timeOrGreeting => {
|
|
13
|
-
if (typeof timeOrGreeting === 'string') {
|
|
14
|
-
return html`<div>${timeOrGreeting}</div>`;
|
|
15
|
-
} else if (timeOrGreeting === 0) {
|
|
16
|
-
return html`<div><b>ALL DONE!</b></div>`;
|
|
17
|
-
} else if (timeOrGreeting === 1) {
|
|
18
|
-
return html`<div><i>ONE second left!!!</i></div>`;
|
|
19
|
-
} else {
|
|
20
|
-
return html`<div>${timeOrGreeting} seconds remaining ...</div>`;
|
|
21
|
-
}
|
|
22
|
-
})}
|
|
23
|
-
</div>`.onadd(self => {
|
|
24
|
-
function tick() {
|
|
25
|
-
self.data.remaining = self.data.remaining - 1;
|
|
26
|
-
if (self.data.remaining > 0) {
|
|
27
|
-
setTimeout(() => tick(), 1000);
|
|
28
|
-
} else {
|
|
29
|
-
hello("So and so").then(r => self.data.remaining = r);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
tick();
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export default Countdown;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
${body}
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
@import url('core.css');
|
|
2
|
-
|
|
3
|
-
/*****************
|
|
4
|
-
BASIC TAGS
|
|
5
|
-
*****************/
|
|
6
|
-
|
|
7
|
-
body {
|
|
8
|
-
margin: 0px;
|
|
9
|
-
padding: 0px;
|
|
10
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
11
|
-
font-size: 11pt;
|
|
12
|
-
line-height: 1.2;
|
|
13
|
-
color: #333333;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
@media (max-width: 50rem) {
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
img {
|
|
20
|
-
border: 0px;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
a, tpdc\:start {
|
|
24
|
-
/* text-decoration: none; */
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
a:hover, tpdc\:start:hover {
|
|
28
|
-
text-decoration: underline;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
a.nohoverdecoration, tpcd\:start.nohoverdecoration {
|
|
32
|
-
text-decoration: none;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
hr {
|
|
36
|
-
margin: 2rem auto;
|
|
37
|
-
width: 65%;
|
|
38
|
-
height: 1px;
|
|
39
|
-
border: 0px;
|
|
40
|
-
background-color: silver;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
#header h1 {
|
|
44
|
-
font-size: 1.6em;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
#header h1 a {
|
|
48
|
-
text-decoration: none;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
#header h1 img {
|
|
52
|
-
vertical-align: text-bottom;
|
|
53
|
-
height: 1.25em;
|
|
54
|
-
border: 2px solid saddlebrown;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/******************
|
|
58
|
-
TEMPLATE PARTS
|
|
59
|
-
******************/
|
|
60
|
-
|
|
61
|
-
#container {
|
|
62
|
-
position: relative;
|
|
63
|
-
width: 90%;
|
|
64
|
-
max-width: 60rem;
|
|
65
|
-
margin: 2rem auto;
|
|
66
|
-
padding: 0.5rem;
|
|
67
|
-
background-color: #f5f5f5;
|
|
68
|
-
border: 1px solid #cccccc;
|
|
69
|
-
border-radius: 0.25em;
|
|
70
|
-
overflow: hidden; /* float containment */
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
#content {
|
|
74
|
-
overflow: hidden;
|
|
75
|
-
padding: 0.85rem;
|
|
76
|
-
padding-top: 0px;
|
|
77
|
-
line-height: 1.3rem;
|
|
78
|
-
text-align: justify;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
#footer {
|
|
82
|
-
clear: both;
|
|
83
|
-
text-align: center;
|
|
84
|
-
font-family: monospace;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
#sub-footer {
|
|
88
|
-
margin-top: 2rem;
|
|
89
|
-
font-size: smaller;
|
|
90
|
-
color: #555555;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.footer-plug {
|
|
94
|
-
margin-top: 2.5rem;
|
|
95
|
-
padding-top: 1.75rem;
|
|
96
|
-
text-align: right;
|
|
97
|
-
color: gray;
|
|
98
|
-
border-top: 1px dashed silver;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.footer-plug a {
|
|
102
|
-
font-size: 1.2rem;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.padded {
|
|
106
|
-
padding: 10px 20px;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
#footer > #sub-footer tpdc\:pagebuildtime {
|
|
110
|
-
font-weight: bold;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.modal {
|
|
114
|
-
position: fixed;
|
|
115
|
-
display: table-cell;
|
|
116
|
-
text-align: center;
|
|
117
|
-
vertical-align: middle;
|
|
118
|
-
overflow: hidden;
|
|
119
|
-
top: 0px;
|
|
120
|
-
left: 0px;
|
|
121
|
-
width: 100%;
|
|
122
|
-
height: 100%;
|
|
123
|
-
margin: 0;
|
|
124
|
-
padding: 1em 0 0 0 ;
|
|
125
|
-
background-color: #ffffff;
|
|
126
|
-
opacity: 0.8;
|
|
127
|
-
filter: alpha(opacity=80);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
tpdc\:teaser {
|
|
131
|
-
display: block;
|
|
132
|
-
margin-top: 2em;
|
|
133
|
-
border-top: 1px dashed silver;
|
|
134
|
-
text-align: right;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
tpdc\:fork {
|
|
138
|
-
display: block;
|
|
139
|
-
position: absolute;
|
|
140
|
-
background-color: transparent;
|
|
141
|
-
top: 0;
|
|
142
|
-
right: 0;
|
|
143
|
-
width: 7em;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
tpdc\:fork > .banner {
|
|
147
|
-
-webkit-transform: rotate(45deg);
|
|
148
|
-
-moz-transform: rotate(45deg);
|
|
149
|
-
-ms-transform: rotate(45deg);
|
|
150
|
-
-o-transform: rotate(45deg);
|
|
151
|
-
transform: rotate(45deg);
|
|
152
|
-
width: 150%;
|
|
153
|
-
text-align: center;
|
|
154
|
-
position: absolute;
|
|
155
|
-
top: 1.5em;
|
|
156
|
-
left: -0.5em;
|
|
157
|
-
font-weight: bold;
|
|
158
|
-
background-color: green;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
tpdc\:fork > .banner > a {
|
|
162
|
-
color: white;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
tpdc\:resultcard {
|
|
166
|
-
display: block;
|
|
167
|
-
border: 15px solid #fce890;
|
|
168
|
-
padding: 15px;
|
|
169
|
-
margin: 15px;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
.result-title {
|
|
173
|
-
text-align: center;
|
|
174
|
-
margin: 0px;
|
|
175
|
-
color: #aa3939;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.result-result {
|
|
179
|
-
text-align: center;
|
|
180
|
-
color: #3399cc;
|
|
181
|
-
border: 2px dashed #99ccee;
|
|
182
|
-
border-radius: 10px;
|
|
183
|
-
padding: 1em;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.result-description {
|
|
187
|
-
text-align: left;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.result-follow {
|
|
191
|
-
text-align: center;
|
|
192
|
-
margin-top: 2em;
|
|
193
|
-
padding-top: 1em;
|
|
194
|
-
border-top: 1px dotted silver;
|
|
195
|
-
vertical-align: bottom;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.result-follow span {
|
|
199
|
-
color: #bb3333;
|
|
200
|
-
font-weight: bold;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
.result-buttons {
|
|
204
|
-
padding-top: 1em;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
@media (max-width:420px) {
|
|
209
|
-
#footer a {
|
|
210
|
-
font-size: larger;
|
|
211
|
-
display: inline-block;
|
|
212
|
-
margin: 0.25em;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.link-list li {
|
|
216
|
-
height: 2em;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.link-list li a {
|
|
220
|
-
font-size: larger;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title>${title + " - example.com"}</title>
|
|
5
|
-
<link rel="shortcut icon" type="image/ico" href="/images/wirejs.svg"/>
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
7
|
-
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
|
8
|
-
${metatags}
|
|
9
|
-
<script src='/layouts/default.js?v=${BUILD_ID}'></script>
|
|
10
|
-
</head>
|
|
11
|
-
<body>
|
|
12
|
-
<div id='container'>
|
|
13
|
-
<div id='header'>
|
|
14
|
-
<h1>
|
|
15
|
-
<a href='/'><img src='images/wirejs.svg' /> home</a> / ${title || "example.com"}
|
|
16
|
-
</h1>
|
|
17
|
-
</div>
|
|
18
|
-
<div class='navbar'><tpdc:menu></tpdc:menu></div>
|
|
19
|
-
<div id='content'>
|
|
20
|
-
${body}
|
|
21
|
-
</div>
|
|
22
|
-
<tpdc:teaser></tpdc:teaser>
|
|
23
|
-
</div>
|
|
24
|
-
<div id='footer'>
|
|
25
|
-
<div id='sub-footer'>
|
|
26
|
-
Do only awesome things.
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<!-- diagrams rendering for markdown files -->
|
|
31
|
-
<script type="module">
|
|
32
|
-
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@9/dist/mermaid.esm.min.mjs';
|
|
33
|
-
[...document.body.getElementsByClassName('language-mermaid')]
|
|
34
|
-
.forEach(node => {
|
|
35
|
-
node.classList.add('mermaid');
|
|
36
|
-
});
|
|
37
|
-
mermaid.init({});
|
|
38
|
-
</script>
|
|
39
|
-
|
|
40
|
-
</body>
|
|
41
|
-
</html>
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { marked } from 'marked';
|
|
2
|
-
import DOMPurify from 'dompurify';
|
|
3
|
-
import { html, id, text, hydrate, node, list, attribute } from 'wirejs-dom/v2';
|
|
4
|
-
import { accountMenu } from '../../components/account-menu.js';
|
|
5
|
-
import { auth, wiki } from 'my-api';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* @param {{
|
|
9
|
-
* content: string | undefined;
|
|
10
|
-
* user: string | undefined;
|
|
11
|
-
* }}
|
|
12
|
-
* @returns
|
|
13
|
-
*/
|
|
14
|
-
async function Wiki({ context }) {
|
|
15
|
-
const filepath = (context || window).location.pathname;
|
|
16
|
-
const content = await wiki.read(context, filepath);
|
|
17
|
-
|
|
18
|
-
const accountMenuNode = accountMenu(auth);
|
|
19
|
-
let markdown = content ?? `This page doesn't exist yet`;
|
|
20
|
-
const signedOutAction = html`<i>(<b>Sign in</b> to edit.)</i>`;
|
|
21
|
-
const signedInAction = html`<button onclick=${enableEditing}>edit</button>`;
|
|
22
|
-
const invisibleDiv = html`<div style='display: none;'></div>`;
|
|
23
|
-
const editor = html`<div>
|
|
24
|
-
<textarea style='width: 20em; height: 10em;' ${id('textarea')}></textarea>
|
|
25
|
-
</div>`;
|
|
26
|
-
|
|
27
|
-
accountMenuNode.data.onchange(async state => {
|
|
28
|
-
if (state.state.user) {
|
|
29
|
-
self.data.actions = signedInAction;
|
|
30
|
-
} else {
|
|
31
|
-
self.data.actions = signedOutAction;
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function enableEditing() {
|
|
36
|
-
editor.data.textarea.value = markdown;
|
|
37
|
-
self.data.editor = editor;
|
|
38
|
-
self.data.actions = html`<div>
|
|
39
|
-
<button onclick=${submitChanges}>save</button>
|
|
40
|
-
<button onclick=${cancelChanges}>cancel</button>
|
|
41
|
-
</div>`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function submitChanges() {
|
|
45
|
-
markdown = editor.data.textarea.value;
|
|
46
|
-
await wiki.write(context, filepath, markdown);
|
|
47
|
-
self.data.content = markdown;
|
|
48
|
-
self.data.actions = signedInAction;
|
|
49
|
-
self.data.editor = invisibleDiv;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function cancelChanges() {
|
|
53
|
-
self.data.actions = signedInAction;
|
|
54
|
-
self.data.editor = html`<div style='display: none;'></div>`;
|
|
55
|
-
self.data.content = markdown;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const self = html`<div id='wiki'>
|
|
59
|
-
<div style='float: right;'>${accountMenuNode}</div>
|
|
60
|
-
${node('content', markdown, md =>
|
|
61
|
-
html`<div>${DOMPurify.sanitize(marked.parse(md))}</div>`)
|
|
62
|
-
}
|
|
63
|
-
${node('editor', invisibleDiv)}
|
|
64
|
-
${node('actions', signedOutAction)}
|
|
65
|
-
</div>`;
|
|
66
|
-
|
|
67
|
-
return self;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
* @param {import('wirejs-services').Context} context
|
|
73
|
-
* @returns
|
|
74
|
-
*/
|
|
75
|
-
export async function generate(context) {
|
|
76
|
-
const visiblePath = context.location.pathname
|
|
77
|
-
.replaceAll('/', ' > ')
|
|
78
|
-
.replaceAll('<', '<')
|
|
79
|
-
.replaceAll('>', '>')
|
|
80
|
-
.replaceAll('-', ' ')
|
|
81
|
-
.replace(/\s+/g, ' ')
|
|
82
|
-
;
|
|
83
|
-
|
|
84
|
-
const page = html`
|
|
85
|
-
<!doctype html>
|
|
86
|
-
<html>
|
|
87
|
-
<head>
|
|
88
|
-
<title>Wiki ${visiblePath}</title>
|
|
89
|
-
</head>
|
|
90
|
-
<body>
|
|
91
|
-
<p><a href='/'>Home</a></p>
|
|
92
|
-
<h1>Wiki ${visiblePath}</h1>
|
|
93
|
-
${await Wiki({ context })}
|
|
94
|
-
</body>
|
|
95
|
-
</html>
|
|
96
|
-
`;
|
|
97
|
-
|
|
98
|
-
return page;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
hydrate('wiki', Wiki);
|