@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,7 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Archive, Song } from './state';
|
|
2
2
|
import state from './state';
|
|
3
3
|
|
|
4
|
-
const PICTURE = 'pictures/
|
|
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
|
|
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('
|
|
22
|
-
|
|
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
|
|
26
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
39
|
-
|
|
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
|
|
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
|
-
|
|
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>
|
|
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">
|
|
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="
|
|
8
|
-
<View direction="row" gap="small">
|
|
9
|
-
<Button onClick="save" disabled="{{ !draft }}">
|
|
10
|
-
<Button variant="secondary" onClick="
|
|
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="{{
|
|
13
|
+
<For each="{{ songs }}" item="song" key="path">
|
|
13
14
|
<View gap="none">
|
|
14
|
-
<Text>{{
|
|
15
|
-
<Text variant="caption" tone="muted">{{
|
|
15
|
+
<Text>{{ song.text }}</Text>
|
|
16
|
+
<Text variant="caption" tone="muted">{{ song.path }}</Text>
|
|
16
17
|
</View>
|
|
17
18
|
</For>
|
|
18
|
-
<If condition="{{
|
|
19
|
-
<Text variant="caption" tone="muted">No
|
|
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">
|
|
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
|
|
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
|
|
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 {
|
|
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.
|
|
11
|
+
return { tab, shown: filter(state.releases, tab, state.genre) };
|
|
12
12
|
},
|
|
13
|
-
|
|
14
|
-
const {
|
|
15
|
-
return {
|
|
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',
|
|
18
|
-
|
|
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(
|
|
23
|
-
return
|
|
24
|
-
(
|
|
25
|
-
(tab === 'all' ||
|
|
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
|
|
2
|
-
export type
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
64
|
-
// The
|
|
65
|
-
shown:
|
|
62
|
+
releases,
|
|
63
|
+
// The releases the tab and the chosen genre leave.
|
|
64
|
+
shown: releases,
|
|
66
65
|
tab: 'all',
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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="
|
|
8
|
-
<Tab value="
|
|
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="{{
|
|
13
|
-
<Chip label="{{ option }}" selected="{{ option ===
|
|
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="
|
|
20
|
-
<ListItem icon="{{
|
|
21
|
-
<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="
|
|
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
|
|
34
|
-
<Tabs variant="segmented" value="{{
|
|
35
|
-
<Tab value="
|
|
36
|
-
<Tab value="
|
|
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
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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 (
|
|
48
|
+
if (Object.values(errors).some(Boolean)) {
|
|
27
49
|
return;
|
|
28
50
|
}
|
|
29
51
|
this.setState({ sending: true });
|
|
30
52
|
try {
|
|
31
|
-
const {
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
25
|
+
tickets: '',
|
|
26
|
+
shoutOut: '',
|
|
27
|
+
ticket: 'regular' as TicketType,
|
|
28
|
+
eTickets: true,
|
|
10
29
|
agreed: false,
|
|
11
|
-
errors: { phone: '',
|
|
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>
|
|
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="
|
|
7
|
-
<Option value="
|
|
8
|
-
<Option value="
|
|
9
|
-
<Option value="
|
|
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="
|
|
12
|
-
<Input label="Email" type="email" placeholder="
|
|
13
|
-
<Input label="Phone" type="phone" placeholder="
|
|
14
|
-
<Input label="
|
|
15
|
-
<TextArea label="
|
|
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="
|
|
20
|
-
<Radio value="
|
|
21
|
-
<Radio value="
|
|
22
|
-
<Radio value="
|
|
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="
|
|
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="
|
|
40
|
+
<Notice tone="success" title="Booked">{{ sent }}</Notice>
|
|
31
41
|
</If>
|
|
32
42
|
<If condition="{{ failure }}">
|
|
33
|
-
<Notice tone="danger" title="Not
|
|
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 }}">
|
|
47
|
+
<Button onClick="send" loading="{{ sending }}" disabled="{{ !agreed }}">Buy tickets</Button>
|
|
38
48
|
</Footer>
|
|
39
49
|
</View>
|