@monimesl/dot-cli 0.1.0-alpha.7 → 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 +158 -71
  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,7 +1,7 @@
1
- import type { Note } from './state';
1
+ import type { Archive, Song } from './state';
2
2
  import state from './state';
3
3
 
4
- const PICTURE = 'pictures/octocat.png';
4
+ const PICTURE = 'pictures/artwork.jpg';
5
5
 
6
6
  export default definePage({
7
7
  initialState: state,
@@ -15,32 +15,51 @@ export default definePage({
15
15
  },
16
16
  actions: {
17
17
  async refresh() {
18
- const entries = await this.dash.file.list('', { recursive: true });
19
- const notes: Note[] = [];
18
+ const entries = await this.dash.storage.file.list('', { recursive: true });
19
+ const songs: Song[] = [];
20
20
  for (const entry of entries) {
21
- if (entry.kind === 'file' && entry.path.startsWith('notes/')) {
22
- notes.push({ path: entry.path, text: await this.dash.file.read(entry.path) });
21
+ if (entry.kind === 'file' && entry.path.startsWith('setlist/')) {
22
+ songs.push({ path: entry.path, text: await this.dash.storage.file.read(entry.path) });
23
23
  }
24
24
  }
25
- const { used, quota } = await this.dash.file.usage();
26
- const kept = await this.dash.file.stat(PICTURE);
25
+ const archives: Archive[] = entries
26
+ .filter((entry) => entry.kind === 'folder' && /^archive\/[^/]+$/.test(entry.path))
27
+ .map((entry) => ({
28
+ path: entry.path,
29
+ time: new Date(Number(entry.name)).toLocaleTimeString('en-GB'),
30
+ }))
31
+ .reverse();
32
+ const { used, quota } = await this.dash.storage.file.usage();
33
+ const kept = await this.dash.storage.file.stat(PICTURE);
27
34
  this.setState({
28
- notes,
35
+ songs,
36
+ archives,
29
37
  usage: `${used} bytes used of ${quota / 1024 / 1024} MB`,
30
- picture: kept ? (await this.dash.file.open(PICTURE)).url : '',
38
+ picture: kept ? (await this.dash.storage.file.open(PICTURE)).url : '',
31
39
  });
32
40
  },
33
41
  async save(state) {
34
- await this.dash.file.write(`notes/${Date.now()}.txt`, state.draft);
42
+ await this.dash.storage.file.write(`setlist/${Date.now()}.txt`, state.draft);
35
43
  this.setState({ draft: '' });
36
44
  await this.actions.refresh();
37
45
  },
38
- async clearNotes() {
39
- await this.dash.file.remove('notes', { recursive: true });
46
+ // Moves the whole setlist folder into the archive, under the time it was archived.
47
+ async archive() {
48
+ await this.dash.storage.file.createFolder('archive');
49
+ await this.dash.storage.file.move('setlist', `archive/${Date.now()}`);
50
+ await this.actions.refresh();
51
+ },
52
+ // Copies an archived setlist back, in place of the current one.
53
+ async copyBack(_state, event: DotEvent<unknown, { path: string }>) {
54
+ await this.dash.storage.file.copy(event.data.path, 'setlist', { overwrite: true });
55
+ await this.actions.refresh();
56
+ },
57
+ async clearSetlist() {
58
+ await this.dash.storage.file.remove('setlist', { recursive: true });
40
59
  await this.actions.refresh();
41
60
  },
42
61
  async forgetPicture() {
43
- await this.dash.file.remove(PICTURE);
62
+ await this.dash.storage.file.remove(PICTURE);
44
63
  await this.actions.refresh();
45
64
  },
46
65
  },
@@ -1,11 +1,19 @@
1
- export interface Note {
1
+ export interface Archive {
2
+ path: string;
3
+ // When it was archived, from its folder's name.
4
+ time: string;
5
+ }
6
+
7
+ export interface Song {
2
8
  path: string;
3
9
  text: string;
4
10
  }
5
11
 
6
12
  export default {
7
13
  draft: '',
8
- notes: [] as Note[],
14
+ songs: [] as Song[],
15
+ // The setlists archived in folders of their own, newest first.
16
+ archives: [] as Archive[],
9
17
  usage: '',
10
18
  picture: '',
11
19
  };
@@ -1,33 +1,48 @@
1
1
  <View>
2
2
  <Text variant="title">Files</Text>
3
- <Text>A dot has a folder of its own that no other dot sees. What it keeps there lasts between runs.</Text>
3
+ <Text>Each dot has its own folder. Other dots can't see it. Files stay after the dot closes.</Text>
4
4
 
5
- <Text variant="caption" tone="muted">Notes, written as text files</Text>
5
+ <Text variant="caption" tone="muted">A setlist. Each song is a text file.</Text>
6
6
  <View variant="card" gap="small">
7
- <Input label="Note" placeholder="Buy rice" value="{{ draft }}" maxLength="80" onChange="setDraft" onSubmit="save" />
8
- <View direction="row" gap="small">
9
- <Button onClick="save" disabled="{{ !draft }}">Save</Button>
10
- <Button variant="secondary" onClick="clearNotes" disabled="{{ notes.length === 0 }}">Delete all</Button>
7
+ <Input label="Song" placeholder="Exodus, by Drizilik" value="{{ draft }}" maxLength="80" onChange="setDraft" onSubmit="save" />
8
+ <View direction="row" gap="small" wrap>
9
+ <Button onClick="save" disabled="{{ !draft }}">Add to the setlist</Button>
10
+ <Button variant="secondary" onClick="archive" disabled="{{ songs.length === 0 }}">Archive it</Button>
11
+ <Button variant="secondary" onClick="clearSetlist" disabled="{{ songs.length === 0 }}">Clear it</Button>
11
12
  </View>
12
- <For each="{{ notes }}" item="note" key="path">
13
+ <For each="{{ songs }}" item="song" key="path">
13
14
  <View gap="none">
14
- <Text>{{ note.text }}</Text>
15
- <Text variant="caption" tone="muted">{{ note.path }}</Text>
15
+ <Text>{{ song.text }}</Text>
16
+ <Text variant="caption" tone="muted">{{ song.path }}</Text>
16
17
  </View>
17
18
  </For>
18
- <If condition="{{ notes.length === 0 }}">
19
- <Text variant="caption" tone="muted">No notes yet</Text>
19
+ <If condition="{{ songs.length === 0 }}">
20
+ <Text variant="caption" tone="muted">No songs yet. Add some, then close and reopen the dot. They are still here.</Text>
20
21
  </If>
21
22
  </View>
22
23
 
23
- <Text variant="caption" tone="muted">A picture kept from the Network page</Text>
24
+ <Text variant="caption" tone="muted">Archived setlists. Each one is a folder you can copy back.</Text>
25
+ <View variant="card" gap="small">
26
+ <If condition="{{ archives.length > 0 }}">
27
+ <List>
28
+ <For each="{{ archives }}" item="archive" key="path">
29
+ <ListItem icon="playlist" title="Archived at {{ archive.time }}" subtitle="{{ archive.path }}" value="Copy back" data-path="{{ archive.path }}" onTap="copyBack" />
30
+ </For>
31
+ </List>
32
+ </If>
33
+ <Else>
34
+ <Text variant="caption" tone="muted">None yet. Add songs, then archive the setlist.</Text>
35
+ </Else>
36
+ </View>
37
+
38
+ <Text variant="caption" tone="muted">Artwork saved from the Network page</Text>
24
39
  <View variant="card" gap="small">
25
40
  <If condition="{{ picture }}">
26
- <Image src="{{ picture }}" alt="Kept picture" width="64" height="64" />
41
+ <Image src="{{ picture }}" alt="Kept artwork" width="64" height="64" />
27
42
  <Button variant="secondary" onClick="forgetPicture">Forget it</Button>
28
43
  </If>
29
44
  <Else>
30
- <Text variant="caption" tone="muted">None yet: download one on the Network page and tap Keep.</Text>
45
+ <Text variant="caption" tone="muted">None yet. On the Network page, search, download the artwork and tap Keep.</Text>
31
46
  </Else>
32
47
  </View>
33
48
 
@@ -1,4 +1,4 @@
1
- import type { Payment } from './state';
1
+ import type { Release } from './state';
2
2
  import state from './state';
3
3
 
4
4
  type Chosen = DotEvent<{ value: string }>;
@@ -8,21 +8,20 @@ export default definePage({
8
8
  mutations: {
9
9
  chooseTab: (state, event: Chosen) => {
10
10
  const tab = event.detail.value;
11
- return { tab, shown: filter(state.payments, tab, state.status) };
11
+ return { tab, shown: filter(state.releases, tab, state.genre) };
12
12
  },
13
- chooseStatus: (state, event: DotEvent<unknown, { status: string }>) => {
14
- const { status } = event.data;
15
- return { status, shown: filter(state.payments, state.tab, status) };
13
+ chooseGenre: (state, event: DotEvent<unknown, { genre: string }>) => {
14
+ const { genre } = event.data;
15
+ return { genre, shown: filter(state.releases, state.tab, genre) };
16
16
  },
17
- clearFilters: (state) => ({ tab: 'all', status: 'All', shown: state.payments }),
18
- choosePeriod: (_state, event: Chosen) => ({ period: event.detail.value }),
17
+ clearFilters: (state) => ({ tab: 'all', genre: 'All', shown: state.releases }),
18
+ chooseWhere: (_state, event: Chosen) => ({ where: event.detail.value }),
19
19
  },
20
20
  });
21
21
 
22
- function filter(payments: readonly Payment[], tab: string, status: string): Payment[] {
23
- return payments.filter(
24
- (payment) =>
25
- (tab === 'all' || payment.direction === tab) &&
26
- (status === 'All' || payment.status === status),
22
+ function filter(releases: readonly Release[], tab: string, genre: string): Release[] {
23
+ return releases.filter(
24
+ (release) =>
25
+ (tab === 'all' || release.kind === tab) && (genre === 'All' || release.genre === genre),
27
26
  );
28
27
  }
@@ -1,72 +1,118 @@
1
- export type Direction = 'in' | 'out';
2
- export type Status = 'Paid' | 'Pending' | 'Failed';
1
+ export type Kind = 'album' | 'song';
2
+ export type Genre = 'Kriofusion' | 'Afropop' | 'Hip hop' | 'R&B' | 'Palm-wine' | 'Gospel' | 'Bubu';
3
3
 
4
- export interface Payment {
4
+ export interface Release {
5
5
  id: string;
6
6
  title: string;
7
+ artist: string;
8
+ year: number;
9
+ kind: Kind;
10
+ genre: Genre;
11
+ }
12
+
13
+ export interface Show {
14
+ id: string;
15
+ title: string;
16
+ place: string;
7
17
  date: string;
8
- amount: string;
9
- direction: Direction;
10
- status: Status;
11
- tone: string;
18
+ city: string;
19
+ }
20
+
21
+ function release(
22
+ id: string,
23
+ title: string,
24
+ artist: string,
25
+ year: number,
26
+ kind: Kind,
27
+ genre: Genre,
28
+ ): Release {
29
+ return { id, title, artist, year, kind, genre };
12
30
  }
13
31
 
14
- const payments: Payment[] = [
15
- {
16
- id: 'p1',
17
- title: 'Salary',
18
- date: 'Today',
19
- amount: 'Le 2,400,000',
20
- direction: 'in',
21
- status: 'Paid',
22
- tone: 'success',
23
- },
24
- {
25
- id: 'p2',
26
- title: 'Electricity',
27
- date: 'Today',
28
- amount: 'Le 50,000',
29
- direction: 'out',
30
- status: 'Paid',
31
- tone: 'success',
32
- },
33
- {
34
- id: 'p3',
35
- title: 'From Aminata',
36
- date: 'Yesterday',
37
- amount: 'Le 75,000',
38
- direction: 'in',
39
- status: 'Pending',
40
- tone: 'warning',
41
- },
42
- {
43
- id: 'p4',
44
- title: 'Airtime',
45
- date: 'Yesterday',
46
- amount: 'Le 10,000',
47
- direction: 'out',
48
- status: 'Pending',
49
- tone: 'warning',
50
- },
51
- {
52
- id: 'p5',
53
- title: 'Water',
54
- date: '12 September',
55
- amount: 'Le 35,000',
56
- direction: 'out',
57
- status: 'Failed',
58
- tone: 'danger',
59
- },
32
+ // Newest first.
33
+ const releases: Release[] = [
34
+ release('audacity', 'AUDACITY', 'Drizilik', 2026, 'album', 'Kriofusion'),
35
+ release('money-or-honey', 'Money or Honey', 'Boii', 2025, 'album', 'R&B'),
36
+ release('control', 'Control', 'ApreeL', 2025, 'song', 'Afropop'),
37
+ release('wait-and-see', 'Wait and See', 'Drizilik', 2025, 'album', 'Kriofusion'),
38
+ release('blunt', 'Blunt', 'Donii Major', 2023, 'song', 'R&B'),
39
+ release('ashobi', 'Ashobi', 'Drizilik', 2022, 'album', 'Kriofusion'),
40
+ release('nack', 'Nack', 'The Therapist', 2022, 'song', 'Afropop'),
41
+ release('hannah', 'Hannah', 'Donii Major', 2021, 'song', 'R&B'),
42
+ release('shukubly', 'Shukubly', 'Drizilik', 2018, 'album', 'Kriofusion'),
43
+ release('en-yay-sah', 'En Yay Sah', 'Janka Nabay and the Bubu Gang', 2012, 'album', 'Bubu'),
44
+ release('red-album', 'Red Album', 'LAJ', 2013, 'album', 'Hip hop'),
45
+ release(
46
+ 'ghetto-struggle',
47
+ 'Ghetto Struggle',
48
+ 'Kao Denero, with Busy Signal',
49
+ 2013,
50
+ 'song',
51
+ 'Hip hop',
52
+ ),
53
+ release('borbor-bele', 'Borbor Bele', 'Emmerson', 2004, 'album', 'Afropop'),
54
+ release('corruption', 'Corruption', 'Daddy Saj', 2003, 'album', 'Hip hop'),
55
+ release('cry-africa', 'Cry Africa', 'Vicky Fornah', 1999, 'song', 'Gospel'),
56
+ release('kormot-bi-en-me', 'Kormot Bi En Me', 'Steady Bongo', 1996, 'album', 'Afropop'),
57
+ release('dead-men', "Dead Men Don't Smoke Marijuana", 'S. E. Rogie', 1994, 'album', 'Palm-wine'),
58
+ release('let-me-love-you', 'Let Me Love You', 'Bunny Mack', 1981, 'song', 'Afropop'),
60
59
  ];
61
60
 
62
61
  export default {
63
- payments,
64
- // The payments the tab and the chosen status leave.
65
- shown: payments,
62
+ releases,
63
+ // The releases the tab and the chosen genre leave.
64
+ shown: releases,
66
65
  tab: 'all',
67
- incoming: payments.filter((payment) => payment.direction === 'in').length,
68
- outgoing: payments.filter((payment) => payment.direction === 'out').length,
69
- statuses: ['All', 'Paid', 'Pending', 'Failed'],
70
- status: 'All',
71
- period: 'week',
66
+ albums: releases.filter((item) => item.kind === 'album').length,
67
+ songs: releases.filter((item) => item.kind === 'song').length,
68
+ genres: ['All', 'Kriofusion', 'Afropop', 'Hip hop', 'R&B', 'Palm-wine', 'Gospel', 'Bubu'],
69
+ genre: 'All',
70
+ // Gen Z's shows, at home in Sierra Leone and abroad.
71
+ where: 'home',
72
+ home: [
73
+ {
74
+ id: 'sledu-2026',
75
+ title: 'SLeDU National Music Awards',
76
+ place: 'Bintumani Conference Centre',
77
+ date: '27 March 2026',
78
+ city: 'Freetown',
79
+ },
80
+ {
81
+ id: 'special-guests-4',
82
+ title: 'Drizilik & Special Guests',
83
+ place: 'Fourah Bay College Amphitheatre',
84
+ date: '28 November 2025',
85
+ city: 'Freetown',
86
+ },
87
+ ] as Show[],
88
+ abroad: [
89
+ {
90
+ id: 'detty-rave',
91
+ title: 'Detty Rave, with Mr Eazi',
92
+ place: 'Drizilik joins him for "Sabi Road"',
93
+ date: 'December 2025',
94
+ city: 'Accra',
95
+ },
96
+ {
97
+ id: 'steel-yard',
98
+ title: 'Drizilik Live In London',
99
+ place: 'The Steel Yard',
100
+ date: '16 November 2025',
101
+ city: 'London',
102
+ },
103
+ {
104
+ id: 'howard-theatre',
105
+ title: 'Drizilik live',
106
+ place: 'Howard Theatre',
107
+ date: '6 July 2024',
108
+ city: 'Washington',
109
+ },
110
+ {
111
+ id: 'freedom-festival',
112
+ title: 'Freedom Festival',
113
+ place: "In Hull, Freetown's twin city",
114
+ date: '2–3 September 2022',
115
+ city: 'Hull',
116
+ },
117
+ ] as Show[],
72
118
  };
@@ -1,43 +1,44 @@
1
1
  <View>
2
2
  <Text variant="title">Tabs and filters</Text>
3
- <Text>Tabs switch what a page shows, chips narrow it down, and an empty state says when nothing is left.</Text>
3
+ <Text>Tabs change what the page shows. Chips narrow it down. An empty state shows when nothing matches. Try Songs with Palm-wine.</Text>
4
4
 
5
5
  <Tabs value="{{ tab }}" onChange="chooseTab">
6
6
  <Tab value="all" label="All" />
7
- <Tab value="in" label="Incoming" badge="{{ incoming }}" />
8
- <Tab value="out" label="Outgoing" badge="{{ outgoing }}" />
7
+ <Tab value="album" label="Albums" badge="{{ albums }}" />
8
+ <Tab value="song" label="Songs" badge="{{ songs }}" />
9
9
  </Tabs>
10
10
 
11
11
  <View direction="row" gap="small" wrap>
12
- <For each="{{ statuses }}" item="option" key="*this">
13
- <Chip label="{{ option }}" selected="{{ option === status }}" data-status="{{ option }}" onTap="chooseStatus" />
12
+ <For each="{{ genres }}" item="option" key="*this">
13
+ <Chip label="{{ option }}" selected="{{ option === genre }}" data-genre="{{ option }}" onTap="chooseGenre" />
14
14
  </For>
15
15
  </View>
16
16
 
17
17
  <If condition="{{ shown.length > 0 }}">
18
18
  <List>
19
- <For each="{{ shown }}" item="payment" key="id">
20
- <ListItem icon="{{ payment.direction === 'in' ? 'arrow-down' : 'arrow-up' }}" title="{{ payment.title }}" subtitle="{{ payment.date }} · {{ payment.amount }}">
21
- <Badge tone="{{ payment.tone }}">{{ payment.status }}</Badge>
19
+ <For each="{{ shown }}" item="release" key="id">
20
+ <ListItem icon="{{ release.kind === 'album' ? 'playlist' : 'music' }}" title="{{ release.title }}" subtitle="{{ release.artist }} · {{ release.genre }}">
21
+ <Badge>{{ release.year }}</Badge>
22
22
  </ListItem>
23
23
  </For>
24
24
  </List>
25
25
  </If>
26
26
  <Else>
27
- <EmptyState icon="search" title="No payments here" text="Nothing matches this tab and status.">
27
+ <EmptyState icon="search" title="Nothing here" text="No release matches this tab and genre.">
28
28
  <Button variant="secondary" onClick="clearFilters">Clear filters</Button>
29
29
  </EmptyState>
30
30
  </Else>
31
31
 
32
32
  <View gap="small">
33
- <Text variant="caption" tone="muted">Segmented tabs, for a few short choices</Text>
34
- <Tabs variant="segmented" value="{{ period }}" onChange="choosePeriod">
35
- <Tab value="week" label="Week" />
36
- <Tab value="month" label="Month" />
37
- <Tab value="year" label="Year" />
33
+ <Text variant="caption" tone="muted">Segmented tabs, for a few short choices. Gen Z shows at home and abroad.</Text>
34
+ <Tabs variant="segmented" value="{{ where }}" onChange="chooseWhere">
35
+ <Tab value="home" label="Sierra Leone" />
36
+ <Tab value="abroad" label="Abroad" />
38
37
  </Tabs>
39
- <View variant="card">
40
- <Text>Spent this {{ period }}: {{ period === 'week' ? 'Le 95,000' : period === 'month' ? 'Le 410,000' : 'Le 4,870,000' }}</Text>
41
- </View>
38
+ <List>
39
+ <For each="{{ where === 'home' ? home : abroad }}" item="show" key="id">
40
+ <ListItem icon="ticket" title="{{ show.title }}" subtitle="{{ show.place }} · {{ show.date }}" value="{{ show.city }}" />
41
+ </For>
42
+ </List>
42
43
  </View>
43
44
  </View>
@@ -1,40 +1,64 @@
1
- import state from './state';
1
+ import type { TicketType } from './state';
2
+ import state, { SHOWS, TICKETS } from './state';
2
3
 
3
4
  type Typed = DotEvent<{ value: string }>;
5
+
6
+ // "2" tickets of a Le 1,200 type -> "Le 2,400"
7
+ function total(tickets: string, ticket: TicketType): string {
8
+ const leones = (Number(tickets) || 0) * TICKETS[ticket].price;
9
+ return `Le ${leones.toLocaleString('en-US')}`;
10
+ }
4
11
  type Toggled = DotEvent<{ checked: boolean }>;
5
12
 
6
13
  export default definePage({
7
14
  initialState: state,
8
15
  mutations: {
9
- setAccount: (_state, event: Typed) => ({ account: event.detail.value }),
16
+ setShow: (_state, event: Typed) => ({
17
+ show: event.detail.value,
18
+ showName: SHOWS[event.detail.value] ?? 'Not chosen yet',
19
+ }),
10
20
  setName: (_state, event: Typed) => ({ name: event.detail.value }),
11
21
  setEmail: (_state, event: Typed) => ({ email: event.detail.value }),
12
22
  setPhone: (_state, event: Typed) => ({ phone: event.detail.value }),
13
- setAmount: (_state, event: Typed) => ({ amount: event.detail.value }),
14
- setNote: (_state, event: Typed) => ({ note: event.detail.value }),
15
- setPlan: (_state, event: Typed) => ({ plan: event.detail.value }),
16
- setReceipts: (_state, event: Toggled) => ({ receipts: event.detail.checked }),
23
+ setTickets: (state, event: Typed) => ({
24
+ tickets: event.detail.value,
25
+ total: total(event.detail.value, state.ticket),
26
+ }),
27
+ setShoutOut: (_state, event: Typed) => ({ shoutOut: event.detail.value }),
28
+ setTicket: (state, event: DotEvent<{ value: TicketType }>) => ({
29
+ ticket: event.detail.value,
30
+ ticketName: TICKETS[event.detail.value].name,
31
+ total: total(state.tickets, event.detail.value),
32
+ }),
33
+ setETickets: (_state, event: Toggled) => ({ eTickets: event.detail.checked }),
17
34
  setAgreed: (_state, event: Toggled) => ({ agreed: event.detail.checked }),
18
35
  },
19
36
  actions: {
20
37
  async send(state) {
38
+ const count = Number(state.tickets);
21
39
  const errors = {
22
- phone: state.phone.length < 8 ? 'Enter a phone number, such as 076 123456' : '',
23
- amount: Number(state.amount) >= 1000 ? '' : 'Send at least Le 1,000',
40
+ show: state.show ? '' : 'Choose a show',
41
+ phone: state.phone.length < 8 ? 'Enter a phone number, such as 76 123456' : '',
42
+ tickets: count >= 1 && count <= 10 ? '' : 'Buy from 1 to 10 tickets',
43
+ shoutOut: state.shoutOut.includes('http')
44
+ ? 'Links are not read out. Write words only.'
45
+ : '',
24
46
  };
25
47
  this.setState({ errors, sent: '', failure: '' });
26
- if (errors.phone || errors.amount) {
48
+ if (Object.values(errors).some(Boolean)) {
27
49
  return;
28
50
  }
29
51
  this.setState({ sending: true });
30
52
  try {
31
- const { account, name, email, phone, amount, note, plan, receipts } = state;
53
+ const { show, name, email, phone, shoutOut, ticket, eTickets } = state;
32
54
  await this.dash.network.http.request({
33
55
  url: 'https://httpbin.org/post',
34
56
  method: 'POST',
35
- body: { account, name, email, phone, amount: Number(amount), note, plan, receipts },
57
+ body: { show, name, email, phone, tickets: count, shoutOut, ticket, eTickets },
58
+ });
59
+ this.setState({
60
+ sent: `${count} ${count === 1 ? 'ticket' : 'tickets'}, sent to +232 ${phone}`,
36
61
  });
37
- this.setState({ sent: `Le ${amount} to ${phone}` });
38
62
  } catch (error) {
39
63
  this.setState({ failure: (error as Error).message });
40
64
  }
@@ -1,14 +1,33 @@
1
+ export type TicketType = 'regular' | 'vip' | 'table';
2
+
3
+ // The made-up shows on sale, and the price of each ticket, in leones.
4
+ export const SHOWS: Record<string, string> = {
5
+ freetown: 'Salone Sounds Live · Freetown',
6
+ bo: 'Palm-wine evening · Bo',
7
+ kenema: 'Gospel night · Kenema',
8
+ };
9
+
10
+ export const TICKETS: Record<TicketType, { name: string; price: number }> = {
11
+ regular: { name: 'Regular', price: 150 },
12
+ vip: { name: 'VIP, near the stage', price: 400 },
13
+ table: { name: 'Table for four', price: 1200 },
14
+ };
15
+
1
16
  export default {
2
- account: '',
17
+ show: '',
18
+ // The order, as the summary shows it.
19
+ showName: 'Not chosen yet',
20
+ ticketName: TICKETS.regular.name,
21
+ total: 'Le 0',
3
22
  name: '',
4
23
  email: '',
5
24
  phone: '',
6
- amount: '',
7
- note: '',
8
- plan: 'monthly',
9
- receipts: true,
25
+ tickets: '',
26
+ shoutOut: '',
27
+ ticket: 'regular' as TicketType,
28
+ eTickets: true,
10
29
  agreed: false,
11
- errors: { phone: '', amount: '' },
30
+ errors: { show: '', phone: '', tickets: '', shoutOut: '' },
12
31
  sending: false,
13
32
  sent: '',
14
33
  failure: '',
@@ -1,39 +1,49 @@
1
1
  <View>
2
2
  <Text variant="title">Forms</Text>
3
- <Text>Each change reaches the controller, which keeps the value in state and checks it when the form is sent.</Text>
3
+ <Text>Buy tickets for a show. The page checks the form when you send it. Some fields turn off based on your choices.</Text>
4
4
 
5
5
  <View variant="card">
6
- <Select label="Pay from" placeholder="Choose an account" value="{{ account }}" onChange="setAccount">
7
- <Option value="savings" label="Savings · Le 1,250,000" />
8
- <Option value="wallet" label="Mobile wallet · Le 84,500" />
9
- <Option value="card" label="Debit card · Le 310,000" />
6
+ <Select label="Show" placeholder="Choose a show" hint="Every show has the same prices" value="{{ show }}" error="{{ errors.show }}" disabled="{{ sending }}" onChange="setShow">
7
+ <Option value="freetown" label="Salone Sounds Live · Freetown" />
8
+ <Option value="bo" label="Palm-wine evening · Bo" />
9
+ <Option value="kenema" label="Gospel night · Kenema" />
10
10
  </Select>
11
- <Input label="Name" placeholder="Ada Lovelace" value="{{ name }}" maxLength="40" onChange="setName" />
12
- <Input label="Email" type="email" placeholder="ada@example.com" hint="For the receipt" value="{{ email }}" onChange="setEmail" />
13
- <Input label="Phone" type="phone" placeholder="076 123456" value="{{ phone }}" error="{{ errors.phone }}" onChange="setPhone" />
14
- <Input label="Amount" type="number" prefix="Le" placeholder="0" value="{{ amount }}" error="{{ errors.amount }}" onChange="setAmount" />
15
- <TextArea label="Note" placeholder="What it's for" rows="3" maxLength="140" hint="{{ note.length }} of 140" value="{{ note }}" onChange="setNote" />
11
+ <Input label="Name" placeholder="Mariama Kamara" value="{{ name }}" maxLength="40" onChange="setName" />
12
+ <Input label="Email" type="email" placeholder="mariama@example.com" hint="{{ eTickets ? 'For the e-tickets' : 'Turn on e-tickets below to use it' }}" value="{{ email }}" disabled="{{ !eTickets }}" onChange="setEmail" />
13
+ <Input label="Phone" type="phone" prefix="+232" placeholder="76 123456" value="{{ phone }}" error="{{ errors.phone }}" onChange="setPhone" />
14
+ <Input label="Tickets" type="number" placeholder="2" suffix="at most 10" value="{{ tickets }}" error="{{ errors.tickets }}" onChange="setTickets" />
15
+ <TextArea label="Shout-out" placeholder="A message for the band, read out on the night" rows="3" maxLength="140" hint="{{ ticket === 'regular' ? 'Shout-outs come with VIP and table tickets' : shoutOut.length + ' of 140' }}" value="{{ shoutOut }}" error="{{ errors.shoutOut }}" disabled="{{ ticket === 'regular' }}" onChange="setShoutOut" />
16
16
  </View>
17
17
 
18
18
  <View variant="card">
19
- <RadioGroup label="How often" value="{{ plan }}" onChange="setPlan">
20
- <Radio value="once" label="Just once" />
21
- <Radio value="monthly" label="Every month" />
22
- <Radio value="yearly" label="Every year" />
19
+ <RadioGroup label="Ticket" value="{{ ticket }}" onChange="setTicket">
20
+ <Radio value="regular" label="Regular · Le 150" />
21
+ <Radio value="vip" label="VIP, near the stage · Le 400" />
22
+ <Radio value="table" label="Table for four · Le 1,200" />
23
23
  </RadioGroup>
24
24
  <Divider />
25
- <Switch label="Send me receipts" checked="{{ receipts }}" onChange="setReceipts" />
25
+ <Switch label="Email me the e-tickets" checked="{{ eTickets }}" onChange="setETickets" />
26
26
  <Checkbox label="I agree to the terms" checked="{{ agreed }}" onChange="setAgreed" />
27
27
  </View>
28
28
 
29
+ <Text variant="caption" tone="muted">Your order</Text>
30
+ <View variant="card" gap="none">
31
+ <DetailRow label="Show" value="{{ showName }}" />
32
+ <DetailRow label="Ticket" value="{{ ticketName }}" />
33
+ <DetailRow label="How many" value="{{ tickets || '0' }}" />
34
+ <Divider />
35
+ <DetailRow label="Total" value="{{ total }}" weight="bold" paddingTop="medium" />
36
+ <DetailRow label="Paid" value="{{ sent ? 'Yes' : 'Not yet' }}" tone="{{ sent ? 'success' : 'warning' }}" />
37
+ </View>
38
+
29
39
  <If condition="{{ sent }}">
30
- <Notice tone="success" title="Sent">{{ sent }}</Notice>
40
+ <Notice tone="success" title="Booked">{{ sent }}</Notice>
31
41
  </If>
32
42
  <If condition="{{ failure }}">
33
- <Notice tone="danger" title="Not sent">{{ failure }}</Notice>
43
+ <Notice tone="danger" title="Not booked">{{ failure }}</Notice>
34
44
  </If>
35
45
 
36
46
  <Footer>
37
- <Button onClick="send" loading="{{ sending }}" disabled="{{ !agreed }}">Send</Button>
47
+ <Button onClick="send" loading="{{ sending }}" disabled="{{ !agreed }}">Buy tickets</Button>
38
48
  </Footer>
39
49
  </View>