@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.
Files changed (57) hide show
  1. package/README.md +17 -9
  2. package/dist/cli.js +156 -67
  3. package/dist/cli.js.map +1 -1
  4. package/dist/{server-DFOUQnLA.js → server-BfPAnsSf.js} +34 -81
  5. package/dist/{server-DFOUQnLA.js.map → server-BfPAnsSf.js.map} +1 -1
  6. package/dist/templates/showcase/.env +1 -1
  7. package/dist/templates/showcase/dot.ts +2 -0
  8. package/dist/templates/showcase/pages/cache/controller.ts +45 -0
  9. package/dist/templates/showcase/pages/cache/state.ts +17 -0
  10. package/dist/templates/showcase/pages/cache/view.dml +36 -0
  11. package/dist/templates/showcase/pages/carousel/controller.ts +33 -3
  12. package/dist/templates/showcase/pages/carousel/state.ts +61 -24
  13. package/dist/templates/showcase/pages/carousel/view.dml +18 -15
  14. package/dist/templates/showcase/pages/device/controller.ts +22 -12
  15. package/dist/templates/showcase/pages/device/state.ts +1 -1
  16. package/dist/templates/showcase/pages/device/view.dml +13 -8
  17. package/dist/templates/showcase/pages/feedback/controller.ts +6 -1
  18. package/dist/templates/showcase/pages/feedback/state.ts +1 -0
  19. package/dist/templates/showcase/pages/feedback/view.dml +23 -23
  20. package/dist/templates/showcase/pages/files/controller.ts +33 -14
  21. package/dist/templates/showcase/pages/files/state.ts +10 -2
  22. package/dist/templates/showcase/pages/files/view.dml +29 -14
  23. package/dist/templates/showcase/pages/filters/controller.ts +11 -12
  24. package/dist/templates/showcase/pages/filters/state.ts +107 -61
  25. package/dist/templates/showcase/pages/filters/view.dml +18 -17
  26. package/dist/templates/showcase/pages/forms/controller.ts +36 -12
  27. package/dist/templates/showcase/pages/forms/state.ts +25 -6
  28. package/dist/templates/showcase/pages/forms/view.dml +28 -18
  29. package/dist/templates/showcase/pages/images/controller.ts +26 -0
  30. package/dist/templates/showcase/pages/images/state.ts +3 -0
  31. package/dist/templates/showcase/pages/images/view.dml +18 -3
  32. package/dist/templates/showcase/pages/index/state.ts +26 -14
  33. package/dist/templates/showcase/pages/index/view.dml +2 -2
  34. package/dist/templates/showcase/pages/layout/controller.ts +36 -2
  35. package/dist/templates/showcase/pages/layout/state.ts +22 -2
  36. package/dist/templates/showcase/pages/layout/view.dml +45 -22
  37. package/dist/templates/showcase/pages/lists/controller.ts +12 -11
  38. package/dist/templates/showcase/pages/lists/state.ts +87 -66
  39. package/dist/templates/showcase/pages/lists/view.dml +15 -15
  40. package/dist/templates/showcase/pages/navigation/controller.ts +2 -1
  41. package/dist/templates/showcase/pages/navigation/view.dml +5 -5
  42. package/dist/templates/showcase/pages/network/controller.ts +68 -24
  43. package/dist/templates/showcase/pages/network/state.ts +13 -6
  44. package/dist/templates/showcase/pages/network/view.dml +29 -18
  45. package/dist/templates/showcase/pages/payments/controller.ts +16 -12
  46. package/dist/templates/showcase/pages/payments/state.ts +3 -0
  47. package/dist/templates/showcase/pages/payments/view.dml +22 -12
  48. package/dist/templates/showcase/pages/secrets/controller.ts +43 -0
  49. package/dist/templates/showcase/pages/secrets/state.ts +5 -0
  50. package/dist/templates/showcase/pages/secrets/view.dml +24 -0
  51. package/dist/templates/showcase/pages/state/state.ts +1 -1
  52. package/dist/templates/showcase/pages/state/view.dml +9 -9
  53. package/dist/templates/showcase/pages/text/controller.ts +3 -3
  54. package/dist/templates/showcase/pages/text/state.ts +6 -5
  55. package/dist/templates/showcase/pages/text/view.dml +22 -13
  56. package/dist/templates/showcase/statics/logo.png +0 -0
  57. 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 draws rows in a card. Small is the default for compact information; medium keeps a roomier hierarchy. A row shows an icon or a picture, a value or anything at its end, and a chevron when it can be tapped.</Text>
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">Default small list; rows know which one was tapped from their data- attributes</Text>
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="{{ accounts }}" item="account" key="id">
9
- <ListItem icon="{{ account.icon }}" title="{{ account.name }}" subtitle="{{ account.number }}" value="{{ account.balance }}" data-id="{{ account.id }}" onTap="choose" />
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">Explicit medium list with a badge at the end of each row</Text>
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="{{ payments }}" item="payment" key="id">
21
- <ListItem title="{{ payment.title }}" subtitle="{{ payment.date }} · {{ payment.amount }}" data-id="{{ payment.id }}" onTap="openPayment">
22
- <Badge tone="{{ payment.tone }}">{{ payment.status }}</Badge>
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, by name, in any tone</Text>
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 between things</Text>
45
+ <Text variant="caption" tone="muted">A divider</Text>
46
46
  <View variant="card">
47
- <Text>Above the line</Text>
47
+ <Text>Side A</Text>
48
48
  <Divider />
49
- <Text>Below the line</Text>
49
+ <Text>Side B</Text>
50
50
  </View>
51
51
  </View>
52
52
 
53
- <Sheet open="{{ selected !== null }}" title="{{ selected.title }} · {{ selected.amount }}" showCloseIcon onClose="closeSheet">
54
- <Button variant="secondary" onClick="copyReference">Copy reference {{ selected.reference }}</Button>
55
- <Button variant="secondary" onClick="shareReceipt">Share receipt</Button>
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
- backgroundColor: colored ? '#1A73E8' : undefined,
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 of the one before it; back returns to the one below. A dot has room for {{ maxDepth }} pages at once.</Text>
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, which each page sets: this one's title is its number</Text>
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' : 'Blue bar' }}</Button>
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 to a page shows its bars again, and leaving the dot puts Dash's own back.</Text>
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: this one runs onHide, then onShow.</Text>
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/octocat.png';
4
+ const PICTURE = 'pictures/artwork.jpg';
4
5
 
5
- interface GitHubUser {
6
- login: string;
7
- name: string | null;
8
- followers: number;
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 fetchZen() {
37
+ async searchAsText(state) {
20
38
  try {
21
- const response = await this.dash.network.http.request({
22
- url: 'https://api.github.com/zen',
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 fetchProfile() {
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<GitHubUser>(
32
- { url: 'https://api.github.com/users/octocat', as: 'json' },
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
- this.setState({
36
- profile: { login: body.login, name: body.name ?? body.login, followers: body.followers },
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: { item: 'rice', count: 2 },
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: 'https://avatars.githubusercontent.com/u/583231?s=128',
59
- name: 'octocat.png',
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: `GitHub answered ${response.status}` });
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: { title: 'Octocat' },
76
- files: [{ field: 'picture', file: state.picture.id }],
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 Profile {
2
- login: string;
3
- name: string;
4
- followers: number;
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
- zen: '',
9
- profile: null as Profile | null,
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 a dot's requests, and only to the hosts its dot.json lists.</Text>
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
- <Button variant="secondary" onClick="fetchZen">Ask GitHub for wisdom</Button>
8
- <If condition="{{ zen }}">
9
- <Text>"{{ zen }}"</Text>
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, parsed with as: 'json'</Text>
14
- <View variant="card" gap="small">
15
- <View direction="row" wrap gap="small">
16
- <Button variant="secondary" onClick="fetchProfile">Load a profile</Button>
17
- <Button variant="secondary" onClick="sendJson">Send JSON</Button>
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
- <If condition="{{ profile }}">
20
- <Text>{{ profile.name }} (@{{ profile.login }}) has {{ profile.followers }} followers</Text>
21
- </If>
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 keep</Text>
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="Octocat" width="64" height="64" />
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>Kept in the dot's own files: see the Files page</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 doesn't list</Text>
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: groupThousands(amount),
19
- request: `https://pay.example.com/request?amount=${amount}&currency=SLE`,
21
+ amountText: (amount / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }),
22
+ request: `https://pay.example.com/tip?amount=${amount}&currency=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
- }
@@ -7,4 +7,7 @@ export default {
7
7
  codeError: '',
8
8
  verified: false,
9
9
  pin: '',
10
+ locked: false,
11
+ pledge: null as number | null,
12
+ pledgeError: '',
10
13
  };
@@ -1,35 +1,45 @@
1
1
  <View>
2
2
  <Text variant="title">Payments</Text>
3
- <Text>Fields for money and codes, and a QR code to be paid with.</Text>
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, grouped as it's typed and sent to the page as a number</Text>
7
- <AmountInput label="Amount to request" currency="Le" value="{{ amount }}" onChange="setAmount" hint="The QR code below asks for it" />
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 pay Le {{ amountText }}" />
11
- <Text tone="muted">Scan to pay Le {{ amountText }}</Text>
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 amount yet" text="Type an amount above to make a QR code for it." />
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">A one-time code: type it, paste it, or let the phone fill it in from an SMS</Text>
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">The payment is confirmed.</Notice>
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="For this page, it's 123456" />
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, hidden as it's typed</Text>
32
- <CodeInput label="PIN" length="4" secure value="{{ pin }}" onChange="setPin" />
33
- <Text tone="muted">{{ pin.length }} of 4 digits</Text>
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,5 @@
1
+ export default {
2
+ name: '',
3
+ signedInAs: '',
4
+ token: '',
5
+ };
@@ -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>
@@ -3,7 +3,7 @@ export interface Tap {
3
3
  }
4
4
 
5
5
  export default {
6
- title: 'taps',
6
+ title: 'cheers for an encore',
7
7
  count: 0,
8
8
  limit: 5,
9
9
  taps: [] as Tap[],
@@ -1,33 +1,33 @@
1
1
  <View>
2
2
  <Text variant="title">State, mutations and actions</Text>
3
- <Text>The page's state lives in the controller. A mutation turns an event into new state; an action can run several, or wait for Dash.</Text>
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>That's the limit</Text>
8
+ <Text>Encore! The band comes back on</Text>
9
9
  </If>
10
10
  <ElseIf condition="{{ count > 0 }}">
11
- <Text>{{ limit - count }} to go</Text>
11
+ <Text>{{ limit - count }} more and the band comes back</Text>
12
12
  </ElseIf>
13
13
  <Else>
14
- <Text>Tap Add one to start</Text>
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 }}">Add one</Button>
18
- <Button variant="secondary" onClick="fillUp" disabled="{{ count >= limit }}">Fill up</Button>
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 tap, newest first</Text>
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 }}. Tap #{{ tap.id }}</Text>
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 taps yet</Text>
30
+ <Text variant="caption" tone="muted">No cheers yet</Text>
31
31
  </If>
32
32
  </View>
33
33
  </View>