@monimesl/dot-cli 0.1.0-alpha.8 → 0.1.0-alpha.9
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/README.md +17 -9
- package/dist/cli.js +156 -67
- package/dist/cli.js.map +1 -1
- package/dist/{server-DFOUQnLA.js → server-BfPAnsSf.js} +34 -81
- package/dist/{server-DFOUQnLA.js.map → server-BfPAnsSf.js.map} +1 -1
- package/dist/templates/showcase/.env +1 -1
- package/dist/templates/showcase/dot.ts +2 -0
- package/dist/templates/showcase/pages/cache/controller.ts +45 -0
- package/dist/templates/showcase/pages/cache/state.ts +17 -0
- package/dist/templates/showcase/pages/cache/view.dml +36 -0
- package/dist/templates/showcase/pages/carousel/controller.ts +33 -3
- package/dist/templates/showcase/pages/carousel/state.ts +61 -24
- package/dist/templates/showcase/pages/carousel/view.dml +18 -15
- package/dist/templates/showcase/pages/device/controller.ts +22 -12
- package/dist/templates/showcase/pages/device/state.ts +1 -1
- package/dist/templates/showcase/pages/device/view.dml +13 -8
- package/dist/templates/showcase/pages/feedback/controller.ts +6 -1
- package/dist/templates/showcase/pages/feedback/state.ts +1 -0
- package/dist/templates/showcase/pages/feedback/view.dml +23 -23
- package/dist/templates/showcase/pages/files/controller.ts +33 -14
- package/dist/templates/showcase/pages/files/state.ts +10 -2
- package/dist/templates/showcase/pages/files/view.dml +29 -14
- package/dist/templates/showcase/pages/filters/controller.ts +11 -12
- package/dist/templates/showcase/pages/filters/state.ts +107 -61
- package/dist/templates/showcase/pages/filters/view.dml +18 -17
- package/dist/templates/showcase/pages/forms/controller.ts +36 -12
- package/dist/templates/showcase/pages/forms/state.ts +25 -6
- package/dist/templates/showcase/pages/forms/view.dml +28 -18
- package/dist/templates/showcase/pages/images/controller.ts +26 -0
- package/dist/templates/showcase/pages/images/state.ts +3 -0
- package/dist/templates/showcase/pages/images/view.dml +18 -3
- package/dist/templates/showcase/pages/index/state.ts +26 -14
- package/dist/templates/showcase/pages/index/view.dml +2 -2
- package/dist/templates/showcase/pages/layout/controller.ts +36 -2
- package/dist/templates/showcase/pages/layout/state.ts +22 -2
- package/dist/templates/showcase/pages/layout/view.dml +45 -22
- package/dist/templates/showcase/pages/lists/controller.ts +12 -11
- package/dist/templates/showcase/pages/lists/state.ts +87 -66
- package/dist/templates/showcase/pages/lists/view.dml +15 -15
- package/dist/templates/showcase/pages/navigation/controller.ts +2 -1
- package/dist/templates/showcase/pages/navigation/view.dml +5 -5
- package/dist/templates/showcase/pages/network/controller.ts +68 -24
- package/dist/templates/showcase/pages/network/state.ts +13 -6
- package/dist/templates/showcase/pages/network/view.dml +29 -18
- package/dist/templates/showcase/pages/payments/controller.ts +16 -12
- package/dist/templates/showcase/pages/payments/state.ts +3 -0
- package/dist/templates/showcase/pages/payments/view.dml +22 -12
- package/dist/templates/showcase/pages/secrets/controller.ts +43 -0
- package/dist/templates/showcase/pages/secrets/state.ts +5 -0
- package/dist/templates/showcase/pages/secrets/view.dml +24 -0
- package/dist/templates/showcase/pages/state/state.ts +1 -1
- package/dist/templates/showcase/pages/state/view.dml +9 -9
- package/dist/templates/showcase/pages/text/controller.ts +3 -3
- package/dist/templates/showcase/pages/text/state.ts +6 -5
- package/dist/templates/showcase/pages/text/view.dml +22 -13
- package/dist/templates/showcase/statics/logo.png +0 -0
- package/package.json +6 -6
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">Lists and icons</Text>
|
|
3
|
-
<Text>A List
|
|
3
|
+
<Text>A List shows rows in a card. Small lists are compact, and medium lists have more room. A row can show an icon or a picture, a value at its end, and an arrow when you can tap it.</Text>
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
|
-
<Text variant="caption" tone="muted">
|
|
6
|
+
<Text variant="caption" tone="muted">Artists in a small list. Tap one: its data- attributes tell the page which one.</Text>
|
|
7
7
|
<List>
|
|
8
|
-
<For each="{{
|
|
9
|
-
<ListItem icon="{{
|
|
8
|
+
<For each="{{ artists }}" item="artist" key="id">
|
|
9
|
+
<ListItem icon="{{ artist.icon }}" title="{{ artist.name }}" subtitle="{{ artist.song }}" value="{{ artist.genre }}" data-id="{{ artist.id }}" onTap="choose" />
|
|
10
10
|
</For>
|
|
11
11
|
</List>
|
|
12
12
|
<If condition="{{ chosen }}">
|
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
</View>
|
|
16
16
|
|
|
17
17
|
<View gap="small">
|
|
18
|
-
<Text variant="caption" tone="muted">
|
|
18
|
+
<Text variant="caption" tone="muted">Famous records in a medium list, each with a badge. Tap one to see its actions.</Text>
|
|
19
19
|
<List size="md">
|
|
20
|
-
<For each="{{
|
|
21
|
-
<ListItem title="{{
|
|
22
|
-
<Badge tone="{{
|
|
20
|
+
<For each="{{ releases }}" item="release" key="id">
|
|
21
|
+
<ListItem title="{{ release.title }}" subtitle="{{ release.artist }} · {{ release.year }}" data-id="{{ release.id }}" onTap="openRelease">
|
|
22
|
+
<Badge tone="{{ release.tone }}">{{ release.note }}</Badge>
|
|
23
23
|
</ListItem>
|
|
24
24
|
</For>
|
|
25
25
|
</List>
|
|
26
26
|
</View>
|
|
27
27
|
|
|
28
28
|
<View gap="small">
|
|
29
|
-
<Text variant="caption" tone="muted">Icons
|
|
29
|
+
<Text variant="caption" tone="muted">Icons by name, in any tone, including music icons</Text>
|
|
30
30
|
<View variant="card" direction="row" wrap gap="large">
|
|
31
31
|
<For each="{{ icons }}" item="icon" key="*this">
|
|
32
32
|
<Icon name="{{ icon }}" />
|
|
@@ -42,17 +42,17 @@
|
|
|
42
42
|
</View>
|
|
43
43
|
|
|
44
44
|
<View gap="small">
|
|
45
|
-
<Text variant="caption" tone="muted">A divider
|
|
45
|
+
<Text variant="caption" tone="muted">A divider</Text>
|
|
46
46
|
<View variant="card">
|
|
47
|
-
<Text>
|
|
47
|
+
<Text>Side A</Text>
|
|
48
48
|
<Divider />
|
|
49
|
-
<Text>
|
|
49
|
+
<Text>Side B</Text>
|
|
50
50
|
</View>
|
|
51
51
|
</View>
|
|
52
52
|
|
|
53
|
-
<Sheet open="{{ selected !== null }}" title="{{ selected.title }} · {{ selected.
|
|
54
|
-
<Button variant="secondary" onClick="
|
|
55
|
-
<Button variant="secondary" onClick="
|
|
53
|
+
<Sheet open="{{ selected !== null }}" title="{{ selected.title }} · {{ selected.year }}" showCloseIcon onClose="closeSheet">
|
|
54
|
+
<Button variant="secondary" onClick="copyTitle">Copy the title</Button>
|
|
55
|
+
<Button variant="secondary" onClick="shareRelease">Share it</Button>
|
|
56
56
|
<Button onClick="closeSheet">Cancel</Button>
|
|
57
57
|
</Sheet>
|
|
58
58
|
<Toast message="{{ toast }}" tone="success" onHide="clearToast" />
|
|
@@ -41,7 +41,8 @@ export default definePage({
|
|
|
41
41
|
const colored = !state.colored;
|
|
42
42
|
// Undefined puts Dash's own colors back.
|
|
43
43
|
this.setNavigationBar({
|
|
44
|
-
|
|
44
|
+
// The green of Sierra Leone's flag.
|
|
45
|
+
backgroundColor: colored ? '#1EB53A' : undefined,
|
|
45
46
|
textColor: colored ? '#FFFFFF' : undefined,
|
|
46
47
|
});
|
|
47
48
|
this.setState({ colored });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="caption" tone="muted">{{ trail }}</Text>
|
|
3
3
|
<Text variant="title">Navigation and hooks</Text>
|
|
4
|
-
<Text>Each page opens on top
|
|
4
|
+
<Text>Each new page opens on top. Back returns to the page below. A dot can have {{ maxDepth }} pages open at once.</Text>
|
|
5
5
|
|
|
6
6
|
<View variant="card" gap="small">
|
|
7
7
|
<Text>This is page {{ depth }} of {{ maxDepth }}</Text>
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
</View>
|
|
15
15
|
</View>
|
|
16
16
|
|
|
17
|
-
<Text variant="caption" tone="muted">Dash's bars
|
|
17
|
+
<Text variant="caption" tone="muted">Each page sets Dash's bars. This page's title is its number.</Text>
|
|
18
18
|
<View variant="card" gap="small">
|
|
19
19
|
<View direction="row" wrap gap="small">
|
|
20
|
-
<Button variant="secondary" onClick="toggleColors">{{ colored ? 'Plain bar' : '
|
|
20
|
+
<Button variant="secondary" onClick="toggleColors">{{ colored ? 'Plain bar' : 'Green bar' }}</Button>
|
|
21
21
|
<Button variant="secondary" onClick="toggleNavigationBar">{{ navigationBarHidden ? 'Show the navigation bar' : 'Hide the navigation bar' }}</Button>
|
|
22
22
|
<Button variant="secondary" onClick="toggleTabBar">{{ tabBarHidden ? 'Show the tab bar' : 'Hide the tab bar' }}</Button>
|
|
23
23
|
</View>
|
|
24
|
-
<Text variant="caption" tone="muted">Going back
|
|
24
|
+
<Text variant="caption" tone="muted">Going back shows that page's bars again. Leaving the dot brings back Dash's bars.</Text>
|
|
25
25
|
</View>
|
|
26
26
|
|
|
27
27
|
<Text variant="caption" tone="muted">The hooks this page ran, in order</Text>
|
|
@@ -29,6 +29,6 @@
|
|
|
29
29
|
<For each="{{ hooks }}" item="run" key="id">
|
|
30
30
|
<Text>{{ run.id }}. {{ run.hook }}</Text>
|
|
31
31
|
</For>
|
|
32
|
-
<Text variant="caption" tone="muted">Open another page and come back
|
|
32
|
+
<Text variant="caption" tone="muted">Open another page and come back. This page runs onHide, then onShow.</Text>
|
|
33
33
|
</View>
|
|
34
34
|
</View>
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import type { Release } from './state';
|
|
1
2
|
import state from './state';
|
|
2
3
|
|
|
3
|
-
const PICTURE = 'pictures/
|
|
4
|
+
const PICTURE = 'pictures/artwork.jpg';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Apple's public catalog search, which needs no key: https://performance-partners.apple.com/search-api
|
|
7
|
+
interface CatalogAnswer {
|
|
8
|
+
results: {
|
|
9
|
+
trackId: number;
|
|
10
|
+
trackName: string;
|
|
11
|
+
collectionName: string;
|
|
12
|
+
releaseDate: string;
|
|
13
|
+
artworkUrl100: string;
|
|
14
|
+
}[];
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
interface Echo {
|
|
@@ -13,38 +19,71 @@ interface Echo {
|
|
|
13
19
|
files: Record<string, string>;
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
// Songs to ask for at a time, and at most.
|
|
23
|
+
const PAGE = 10;
|
|
24
|
+
const MOST = 50;
|
|
25
|
+
|
|
26
|
+
function searchUrl(artist: string, limit = PAGE): string {
|
|
27
|
+
const term = encodeURIComponent(artist);
|
|
28
|
+
return `https://itunes.apple.com/search?entity=song&limit=${limit}&term=${term}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export default definePage({
|
|
17
32
|
initialState: state,
|
|
33
|
+
mutations: {
|
|
34
|
+
setArtist: (_state, event: DotEvent<{ value: string }>) => ({ artist: event.detail.value }),
|
|
35
|
+
},
|
|
18
36
|
actions: {
|
|
19
|
-
async
|
|
37
|
+
async searchAsText(state) {
|
|
20
38
|
try {
|
|
21
|
-
const response = await this.dash.network.http.request({
|
|
22
|
-
|
|
39
|
+
const response = await this.dash.network.http.request({ url: searchUrl(state.artist) });
|
|
40
|
+
this.setState({
|
|
41
|
+
raw: response.ok
|
|
42
|
+
? `${response.body.trim().slice(0, 90)}…`
|
|
43
|
+
: `Apple answered ${response.status}`,
|
|
23
44
|
});
|
|
24
|
-
this.setState({ zen: response.ok ? response.body : `GitHub answered ${response.status}` });
|
|
25
45
|
} catch (error) {
|
|
26
46
|
this.setState({ note: (error as Error).message });
|
|
27
47
|
}
|
|
28
48
|
},
|
|
29
|
-
async
|
|
49
|
+
async search() {
|
|
50
|
+
this.setState({ limit: PAGE, releases: [] });
|
|
51
|
+
await this.actions.load();
|
|
52
|
+
},
|
|
53
|
+
// The List asks for this as its last song comes into view: the next page, while there is one.
|
|
54
|
+
async loadMore(state) {
|
|
55
|
+
if (state.searching || state.releases.length < state.limit || state.limit >= MOST) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.setState({ limit: state.limit + PAGE });
|
|
59
|
+
await this.actions.load();
|
|
60
|
+
},
|
|
61
|
+
async load(state) {
|
|
62
|
+
this.setState({ searching: true, note: '' });
|
|
30
63
|
try {
|
|
31
|
-
const { body } = await this.dash.network.http.request<
|
|
32
|
-
{ url:
|
|
64
|
+
const { body } = await this.dash.network.http.request<CatalogAnswer>(
|
|
65
|
+
{ url: searchUrl(state.artist, state.limit), as: 'json' },
|
|
33
66
|
{ timeout: 10_000 },
|
|
34
67
|
);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
const releases: Release[] = body.results.map((result) => ({
|
|
69
|
+
id: result.trackId,
|
|
70
|
+
title: result.trackName,
|
|
71
|
+
album: result.collectionName,
|
|
72
|
+
year: result.releaseDate.slice(0, 4),
|
|
73
|
+
artwork: result.artworkUrl100,
|
|
74
|
+
}));
|
|
75
|
+
this.setState({ releases });
|
|
38
76
|
} catch (error) {
|
|
39
77
|
this.setState({ note: (error as Error).message });
|
|
40
78
|
}
|
|
79
|
+
this.setState({ searching: false });
|
|
41
80
|
},
|
|
42
|
-
async sendJson() {
|
|
81
|
+
async sendJson(state) {
|
|
43
82
|
try {
|
|
44
83
|
const { body } = await this.dash.network.http.request<Echo>({
|
|
45
84
|
url: 'https://httpbin.org/post',
|
|
46
85
|
method: 'POST',
|
|
47
|
-
body: {
|
|
86
|
+
body: { artist: state.artist, favourite: state.releases[0]?.title ?? null },
|
|
48
87
|
as: 'json',
|
|
49
88
|
});
|
|
50
89
|
this.setState({ echo: JSON.stringify(body.json) });
|
|
@@ -52,17 +91,22 @@ export default definePage({
|
|
|
52
91
|
this.setState({ note: (error as Error).message });
|
|
53
92
|
}
|
|
54
93
|
},
|
|
55
|
-
async download() {
|
|
94
|
+
async download(state) {
|
|
95
|
+
const release = state.releases[0];
|
|
96
|
+
if (!release) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
56
99
|
try {
|
|
100
|
+
// The catalog's artwork comes in any size its address names.
|
|
57
101
|
const response = await this.dash.network.http.download({
|
|
58
|
-
url:
|
|
59
|
-
name: '
|
|
102
|
+
url: release.artwork.replace('100x100bb', '300x300bb'),
|
|
103
|
+
name: 'artwork.jpg',
|
|
60
104
|
});
|
|
61
105
|
if (response.ok) {
|
|
62
106
|
const { id, url, size } = response.file;
|
|
63
107
|
this.setState({ picture: { id, url, size }, kept: false });
|
|
64
108
|
} else {
|
|
65
|
-
this.setState({ note: `
|
|
109
|
+
this.setState({ note: `Apple answered ${response.status}` });
|
|
66
110
|
}
|
|
67
111
|
} catch (error) {
|
|
68
112
|
this.setState({ note: (error as Error).message });
|
|
@@ -72,8 +116,8 @@ export default definePage({
|
|
|
72
116
|
try {
|
|
73
117
|
const { body } = await this.dash.network.http.upload<Echo>({
|
|
74
118
|
url: 'https://httpbin.org/post',
|
|
75
|
-
fields: {
|
|
76
|
-
files: [{ field: '
|
|
119
|
+
fields: { artist: state.artist },
|
|
120
|
+
files: [{ field: 'artwork', file: state.picture.id }],
|
|
77
121
|
as: 'json',
|
|
78
122
|
});
|
|
79
123
|
this.setState({ uploaded: Object.keys(body.files).join(', ') });
|
|
@@ -90,7 +134,7 @@ export default definePage({
|
|
|
90
134
|
},
|
|
91
135
|
async keep(state) {
|
|
92
136
|
try {
|
|
93
|
-
await this.dash.file.save(state.picture.id, PICTURE, { overwrite: true });
|
|
137
|
+
await this.dash.storage.file.save(state.picture.id, PICTURE, { overwrite: true });
|
|
94
138
|
this.setState({ kept: true });
|
|
95
139
|
} catch (error) {
|
|
96
140
|
this.setState({ note: (error as Error).message });
|
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
export interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export interface Release {
|
|
2
|
+
id: number;
|
|
3
|
+
title: string;
|
|
4
|
+
album: string;
|
|
5
|
+
year: string;
|
|
6
|
+
artwork: string;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
export default {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
artist: 'Drizilik',
|
|
11
|
+
// The start of Apple's answer, as the text it came as.
|
|
12
|
+
raw: '',
|
|
13
|
+
releases: [] as Release[],
|
|
14
|
+
// How many songs to ask for: more each time the list's end comes into view.
|
|
15
|
+
limit: 10,
|
|
16
|
+
searching: false,
|
|
10
17
|
echo: '',
|
|
11
18
|
picture: { id: '', url: '', size: 0 },
|
|
12
19
|
uploaded: '',
|
|
@@ -1,40 +1,51 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">Network</Text>
|
|
3
|
-
<Text>Dash sends
|
|
3
|
+
<Text>Dash sends the dot's requests, only to hosts listed in dot.json. This page searches Apple's music catalog.</Text>
|
|
4
4
|
|
|
5
|
-
<Text variant="caption" tone="muted">A request, read as text</Text>
|
|
6
5
|
<View variant="card" gap="small">
|
|
7
|
-
<
|
|
8
|
-
<
|
|
9
|
-
<
|
|
6
|
+
<Input label="Artist" placeholder="Drizilik" value="{{ artist }}" maxLength="40" onChange="setArtist" onSubmit="search" />
|
|
7
|
+
<View direction="row" wrap gap="small">
|
|
8
|
+
<Button onClick="search" loading="{{ searching }}" disabled="{{ !artist }}">Search</Button>
|
|
9
|
+
<Button variant="secondary" onClick="searchAsText" disabled="{{ !artist }}">Read as text</Button>
|
|
10
|
+
</View>
|
|
11
|
+
<If condition="{{ raw }}">
|
|
12
|
+
<Text variant="caption" tone="muted">The answer as text starts with: {{ raw }}</Text>
|
|
10
13
|
</If>
|
|
11
14
|
</View>
|
|
12
15
|
|
|
13
|
-
<Text variant="caption" tone="muted">JSON,
|
|
14
|
-
<
|
|
15
|
-
<
|
|
16
|
-
<
|
|
17
|
-
|
|
16
|
+
<Text variant="caption" tone="muted">The same answer read as JSON, with artwork. Near the end, the list loads 10 more, up to 50. {{ releases.length }} so far.</Text>
|
|
17
|
+
<If condition="{{ releases.length > 0 }}">
|
|
18
|
+
<List onEndReached="loadMore">
|
|
19
|
+
<For each="{{ releases }}" item="release" key="id">
|
|
20
|
+
<ListItem image="{{ release.artwork }}" title="{{ release.title }}" subtitle="{{ release.album }} · {{ release.year }}" />
|
|
21
|
+
</For>
|
|
22
|
+
</List>
|
|
23
|
+
</If>
|
|
24
|
+
<Else>
|
|
25
|
+
<View variant="card">
|
|
26
|
+
<EmptyState icon="search" title="No songs yet" text="Search for an artist, such as Drizilik, Kao Denero or Emmerson." />
|
|
18
27
|
</View>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
</Else>
|
|
29
|
+
|
|
30
|
+
<Text variant="caption" tone="muted">JSON sent to httpbin, which sends it back</Text>
|
|
31
|
+
<View variant="card" gap="small">
|
|
32
|
+
<Button variant="secondary" onClick="sendJson">Send the first song as JSON</Button>
|
|
22
33
|
<If condition="{{ echo }}">
|
|
23
34
|
<Text>httpbin got {{ echo }}</Text>
|
|
24
35
|
</If>
|
|
25
36
|
</View>
|
|
26
37
|
|
|
27
|
-
<Text variant="caption" tone="muted">Files: download, upload and
|
|
38
|
+
<Text variant="caption" tone="muted">Files: download the first song's artwork, upload it, and save it</Text>
|
|
28
39
|
<View variant="card" gap="small">
|
|
29
40
|
<View direction="row" wrap gap="small">
|
|
30
|
-
<Button variant="secondary" onClick="download">Download</Button>
|
|
41
|
+
<Button variant="secondary" onClick="download" disabled="{{ releases.length === 0 }}">Download</Button>
|
|
31
42
|
<Button variant="secondary" onClick="upload" disabled="{{ !picture.id }}">Upload</Button>
|
|
32
43
|
<Button variant="secondary" onClick="sharePicture" disabled="{{ !picture.id }}">Share</Button>
|
|
33
44
|
<Button variant="secondary" onClick="keep" disabled="{{ !picture.id || kept }}">Keep</Button>
|
|
34
45
|
</View>
|
|
35
46
|
<If condition="{{ picture.url }}">
|
|
36
47
|
<View direction="row" align="center" gap="medium">
|
|
37
|
-
<Image src="{{ picture.url }}" alt="
|
|
48
|
+
<Image src="{{ picture.url }}" alt="Artwork" width="64" height="64" />
|
|
38
49
|
<Text variant="caption" tone="muted">{{ picture.size }} bytes, in a temporary file until the dot closes</Text>
|
|
39
50
|
</View>
|
|
40
51
|
</If>
|
|
@@ -42,11 +53,11 @@
|
|
|
42
53
|
<Text>httpbin got the file field "{{ uploaded }}"</Text>
|
|
43
54
|
</If>
|
|
44
55
|
<If condition="{{ kept }}">
|
|
45
|
-
<Text>
|
|
56
|
+
<Text>Saved in the dot's files. See the Files page.</Text>
|
|
46
57
|
</If>
|
|
47
58
|
</View>
|
|
48
59
|
|
|
49
|
-
<Text variant="caption" tone="muted">A host dot.json
|
|
60
|
+
<Text variant="caption" tone="muted">A host that dot.json does not list</Text>
|
|
50
61
|
<View variant="card" gap="small">
|
|
51
62
|
<Button variant="secondary" onClick="tryUnlistedHost">Reach example.com</Button>
|
|
52
63
|
<If condition="{{ note }}">
|
|
@@ -5,6 +5,9 @@ type Typed = DotEvent<{ value: string }>;
|
|
|
5
5
|
// The code this page accepts; a real dot checks it with its server.
|
|
6
6
|
const RIGHT_CODE = '123456';
|
|
7
7
|
|
|
8
|
+
// In dollars, as the pledge field sends them.
|
|
9
|
+
const MAX_PLEDGE = 1000;
|
|
10
|
+
|
|
8
11
|
export default definePage({
|
|
9
12
|
initialState: state,
|
|
10
13
|
mutations: {
|
|
@@ -15,8 +18,8 @@ export default definePage({
|
|
|
15
18
|
}
|
|
16
19
|
return {
|
|
17
20
|
amount,
|
|
18
|
-
amountText:
|
|
19
|
-
request: `https://pay.example.com/
|
|
21
|
+
amountText: (amount / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }),
|
|
22
|
+
request: `https://pay.example.com/tip?amount=${amount}¤cy=SLE&in=cents`,
|
|
20
23
|
};
|
|
21
24
|
},
|
|
22
25
|
setCode: (_state, event: Typed) => ({ code: event.detail.value, codeError: '' }),
|
|
@@ -28,15 +31,16 @@ export default definePage({
|
|
|
28
31
|
},
|
|
29
32
|
startAgain: () => ({ verified: false, code: '' }),
|
|
30
33
|
setPin: (_state, event: Typed) => ({ pin: event.detail.value }),
|
|
34
|
+
clearPin: () => ({ pin: '' }),
|
|
35
|
+
setLocked: (_state, event: DotEvent<{ checked: boolean }>) => ({
|
|
36
|
+
locked: event.detail.checked,
|
|
37
|
+
}),
|
|
38
|
+
setPledge: (_state, event: DotEvent<{ value: number | null }>) => {
|
|
39
|
+
const pledge = event.detail.value;
|
|
40
|
+
return {
|
|
41
|
+
pledge,
|
|
42
|
+
pledgeError: pledge !== null && pledge > MAX_PLEDGE ? 'Pledge up to USD 1,000' : '',
|
|
43
|
+
};
|
|
44
|
+
},
|
|
31
45
|
},
|
|
32
46
|
});
|
|
33
|
-
|
|
34
|
-
// 1250000 -> "1,250,000"
|
|
35
|
-
function groupThousands(amount: number): string {
|
|
36
|
-
const digits = String(Math.round(amount));
|
|
37
|
-
const groups: string[] = [];
|
|
38
|
-
for (let end = digits.length; end > 0; end -= 3) {
|
|
39
|
-
groups.unshift(digits.slice(Math.max(0, end - 3), end));
|
|
40
|
-
}
|
|
41
|
-
return groups.join(',');
|
|
42
|
-
}
|
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">Payments</Text>
|
|
3
|
-
<Text>Fields for money and codes
|
|
3
|
+
<Text>Fields for money and codes. Tip the band with a QR code. Confirm tickets with a code.</Text>
|
|
4
4
|
|
|
5
5
|
<View gap="small">
|
|
6
|
-
<Text variant="caption" tone="muted">An amount
|
|
7
|
-
<AmountInput label="
|
|
6
|
+
<Text variant="caption" tone="muted">An amount. It is formatted as you type and sent as a number.</Text>
|
|
7
|
+
<AmountInput label="Tip for the band" value="{{ amount }}" disabled="{{ locked }}" onChange="setAmount" hint="The QR code below asks for it" />
|
|
8
|
+
<Switch label="Lock the tip" checked="{{ locked }}" onChange="setLocked" />
|
|
8
9
|
<View variant="card" align="center" gap="medium">
|
|
9
10
|
<If condition="{{ request }}">
|
|
10
|
-
<QRCode value="{{ request }}" size="180" label="A QR code to
|
|
11
|
-
<Text tone="muted">Scan to
|
|
11
|
+
<QRCode value="{{ request }}" size="180" label="A QR code to tip SLE {{ amountText }}" />
|
|
12
|
+
<Text tone="muted">Scan to tip the band SLE {{ amountText }}</Text>
|
|
12
13
|
</If>
|
|
13
14
|
<Else>
|
|
14
|
-
<EmptyState icon="qr-code" title="No
|
|
15
|
+
<EmptyState icon="qr-code" title="No tip yet" text="Type an amount above to make a QR code the crowd can scan." />
|
|
15
16
|
</Else>
|
|
16
17
|
</View>
|
|
17
18
|
</View>
|
|
18
19
|
|
|
19
20
|
<View gap="small">
|
|
20
|
-
<Text variant="caption" tone="muted">
|
|
21
|
+
<Text variant="caption" tone="muted">Another currency: US dollars. This one sends dollars, not cents</Text>
|
|
22
|
+
<AmountInput label="Pledge for the band's tour" currency="USD" unit="major" value="{{ pledge }}" error="{{ pledgeError }}" hint="Up to USD 1,000" onChange="setPledge" />
|
|
23
|
+
</View>
|
|
24
|
+
|
|
25
|
+
<View gap="small">
|
|
26
|
+
<Text variant="caption" tone="muted">A one-time code. Type it, paste it, or let the phone fill it from an SMS.</Text>
|
|
21
27
|
<If condition="{{ verified }}">
|
|
22
|
-
<Notice tone="success" title="Code accepted">
|
|
28
|
+
<Notice tone="success" title="Code accepted">Your two tickets are confirmed.</Notice>
|
|
23
29
|
<Button variant="secondary" onClick="startAgain">Start again</Button>
|
|
24
30
|
</If>
|
|
25
31
|
<Else>
|
|
26
|
-
<CodeInput label="The code we sent to 076 •••• 21" value="{{ code }}" onChange="setCode" onComplete="checkCode" error="{{ codeError }}" hint="
|
|
32
|
+
<CodeInput label="The code we sent to 076 •••• 21" value="{{ code }}" onChange="setCode" onComplete="checkCode" error="{{ codeError }}" hint="Use 123456" />
|
|
27
33
|
</Else>
|
|
28
34
|
</View>
|
|
29
35
|
|
|
30
36
|
<View gap="small">
|
|
31
|
-
<Text variant="caption" tone="muted">A PIN
|
|
32
|
-
<CodeInput label="PIN" length="4" secure value="{{ pin }}" onChange="setPin" />
|
|
33
|
-
<
|
|
37
|
+
<Text variant="caption" tone="muted">A PIN. It is hidden as you type and locks when complete.</Text>
|
|
38
|
+
<CodeInput label="PIN" length="4" secure value="{{ pin }}" disabled="{{ pin.length === 4 }}" onChange="setPin" />
|
|
39
|
+
<View direction="row" align="center" gap="small">
|
|
40
|
+
<Text tone="muted">{{ pin.length }} of 4 digits</Text>
|
|
41
|
+
<View grow />
|
|
42
|
+
<Button variant="secondary" onClick="clearPin" disabled="{{ pin.length === 0 }}">Clear the PIN</Button>
|
|
43
|
+
</View>
|
|
34
44
|
</View>
|
|
35
45
|
</View>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
const SESSION = 'session';
|
|
4
|
+
const NAME = 'session-name';
|
|
5
|
+
|
|
6
|
+
export default definePage({
|
|
7
|
+
initialState: state,
|
|
8
|
+
hooks: {
|
|
9
|
+
async onShow() {
|
|
10
|
+
await this.actions.refresh();
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
mutations: {
|
|
14
|
+
setName: (_state, event: DotEvent<{ value: string }>) => ({ name: event.detail.value }),
|
|
15
|
+
},
|
|
16
|
+
actions: {
|
|
17
|
+
async refresh() {
|
|
18
|
+
const token = await this.dash.storage.secret.get(SESSION);
|
|
19
|
+
const signedInAs = await this.dash.storage.cache.get<string>(NAME);
|
|
20
|
+
this.setState({
|
|
21
|
+
token: token ? `${token.slice(0, 12)}…` : '',
|
|
22
|
+
signedInAs: signedInAs ?? '',
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
async signIn(state) {
|
|
26
|
+
// A real dot gets its token from its own API, once the person signs in there.
|
|
27
|
+
const token = `demo-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
28
|
+
await this.dash.storage.secret.set(SESSION, token);
|
|
29
|
+
await this.dash.storage.cache.set(NAME, state.name);
|
|
30
|
+
this.setState({ name: '' });
|
|
31
|
+
await this.actions.refresh();
|
|
32
|
+
},
|
|
33
|
+
async signOut() {
|
|
34
|
+
await this.dash.storage.secret.remove(SESSION);
|
|
35
|
+
await this.dash.storage.cache.remove(NAME);
|
|
36
|
+
await this.actions.refresh();
|
|
37
|
+
},
|
|
38
|
+
async forgetAll() {
|
|
39
|
+
await this.dash.storage.secret.clear();
|
|
40
|
+
await this.actions.refresh();
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Secrets</Text>
|
|
3
|
+
<Text>Secrets hold sign-in tokens from the dot's own server. Dash keeps them encrypted on the phone. A dot can keep up to 8.</Text>
|
|
4
|
+
|
|
5
|
+
<Text variant="caption" tone="muted">A sign-in that stays</Text>
|
|
6
|
+
<View variant="card" gap="small">
|
|
7
|
+
<If condition="{{ token }}">
|
|
8
|
+
<Text>Signed in as {{ signedInAs }}</Text>
|
|
9
|
+
<Text variant="caption" tone="muted">Session token {{ token }}</Text>
|
|
10
|
+
<View direction="row" gap="small">
|
|
11
|
+
<Button variant="secondary" onClick="signOut">Sign out</Button>
|
|
12
|
+
<Button variant="secondary" onClick="forgetAll">Forget every secret</Button>
|
|
13
|
+
</View>
|
|
14
|
+
</If>
|
|
15
|
+
<Else>
|
|
16
|
+
<Input label="Name" placeholder="Mariama" value="{{ name }}" maxLength="40" onChange="setName" onSubmit="signIn" />
|
|
17
|
+
<Button onClick="signIn" disabled="{{ !name }}">Sign in</Button>
|
|
18
|
+
<Text variant="caption" tone="muted">Sign in, then close and reopen the dot. You stay signed in.</Text>
|
|
19
|
+
</Else>
|
|
20
|
+
</View>
|
|
21
|
+
|
|
22
|
+
<Text variant="caption" tone="muted">Your name is saved in the cache. Only the token is a secret.</Text>
|
|
23
|
+
<Text variant="caption" tone="muted">Never put your API keys in the dot's code: it is open to read.</Text>
|
|
24
|
+
</View>
|
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
<View>
|
|
2
2
|
<Text variant="title">State, mutations and actions</Text>
|
|
3
|
-
<Text>The page
|
|
3
|
+
<Text>The page keeps its data in state. A mutation changes state when something happens. An action can run several steps or wait for Dash. Here, the crowd wants one more song.</Text>
|
|
4
4
|
|
|
5
5
|
<View variant="card" gap="small">
|
|
6
6
|
<Text variant="title">{{ count }} of {{ limit }} {{ title }}</Text>
|
|
7
7
|
<If condition="{{ count >= limit }}">
|
|
8
|
-
<Text>
|
|
8
|
+
<Text>Encore! The band comes back on</Text>
|
|
9
9
|
</If>
|
|
10
10
|
<ElseIf condition="{{ count > 0 }}">
|
|
11
|
-
<Text>{{ limit - count }}
|
|
11
|
+
<Text>{{ limit - count }} more and the band comes back</Text>
|
|
12
12
|
</ElseIf>
|
|
13
13
|
<Else>
|
|
14
|
-
<Text>Tap
|
|
14
|
+
<Text>Tap Cheer to start</Text>
|
|
15
15
|
</Else>
|
|
16
16
|
<View direction="row" wrap gap="small">
|
|
17
|
-
<Button onClick="increment" disabled="{{ count >= limit }}">
|
|
18
|
-
<Button variant="secondary" onClick="fillUp" disabled="{{ count >= limit }}">
|
|
17
|
+
<Button onClick="increment" disabled="{{ count >= limit }}">Cheer</Button>
|
|
18
|
+
<Button variant="secondary" onClick="fillUp" disabled="{{ count >= limit }}">Whole crowd</Button>
|
|
19
19
|
<Button variant="secondary" onClick="undo" disabled="{{ count === 0 }}">Undo</Button>
|
|
20
20
|
<Button variant="secondary" onClick="reset" disabled="{{ count === 0 }}">Reset</Button>
|
|
21
21
|
</View>
|
|
22
22
|
</View>
|
|
23
23
|
|
|
24
|
-
<Text variant="caption" tone="muted">Each
|
|
24
|
+
<Text variant="caption" tone="muted">Each cheer, newest first</Text>
|
|
25
25
|
<View variant="card" gap="small">
|
|
26
26
|
<For each="{{ taps }}" index="position" item="tap" key="id">
|
|
27
|
-
<Text>{{ position + 1 }}.
|
|
27
|
+
<Text>{{ position + 1 }}. Cheer #{{ tap.id }}</Text>
|
|
28
28
|
</For>
|
|
29
29
|
<If condition="{{ taps.length === 0 }}">
|
|
30
|
-
<Text variant="caption" tone="muted">No
|
|
30
|
+
<Text variant="caption" tone="muted">No cheers yet</Text>
|
|
31
31
|
</If>
|
|
32
32
|
</View>
|
|
33
33
|
</View>
|