@monimesl/dot-cli 0.1.0-alpha.10
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 +96 -0
- package/bin/dotctl.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +942 -0
- package/dist/cli.js.map +1 -0
- package/dist/server-Dbrgkelo.js +11215 -0
- package/dist/server-Dbrgkelo.js.map +1 -0
- package/dist/templates/showcase/.env +3 -0
- package/dist/templates/showcase/dot.ts +24 -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 +40 -0
- package/dist/templates/showcase/pages/carousel/state.ts +77 -0
- package/dist/templates/showcase/pages/carousel/view.dml +40 -0
- package/dist/templates/showcase/pages/device/controller.ts +80 -0
- package/dist/templates/showcase/pages/device/state.ts +7 -0
- package/dist/templates/showcase/pages/device/view.dml +49 -0
- package/dist/templates/showcase/pages/feedback/controller.ts +21 -0
- package/dist/templates/showcase/pages/feedback/state.ts +8 -0
- package/dist/templates/showcase/pages/feedback/view.dml +87 -0
- package/dist/templates/showcase/pages/files/controller.ts +66 -0
- package/dist/templates/showcase/pages/files/state.ts +19 -0
- package/dist/templates/showcase/pages/files/view.dml +50 -0
- package/dist/templates/showcase/pages/filters/controller.ts +27 -0
- package/dist/templates/showcase/pages/filters/state.ts +118 -0
- package/dist/templates/showcase/pages/filters/view.dml +44 -0
- package/dist/templates/showcase/pages/forms/controller.ts +68 -0
- package/dist/templates/showcase/pages/forms/state.ts +34 -0
- package/dist/templates/showcase/pages/forms/view.dml +49 -0
- package/dist/templates/showcase/pages/images/controller.ts +41 -0
- package/dist/templates/showcase/pages/images/state.ts +10 -0
- package/dist/templates/showcase/pages/images/view.dml +53 -0
- package/dist/templates/showcase/pages/index/controller.ts +10 -0
- package/dist/templates/showcase/pages/index/state.ts +128 -0
- package/dist/templates/showcase/pages/index/view.dml +20 -0
- package/dist/templates/showcase/pages/layout/controller.ts +60 -0
- package/dist/templates/showcase/pages/layout/state.ts +34 -0
- package/dist/templates/showcase/pages/layout/view.dml +103 -0
- package/dist/templates/showcase/pages/lists/controller.ts +37 -0
- package/dist/templates/showcase/pages/lists/state.ts +117 -0
- package/dist/templates/showcase/pages/lists/view.dml +59 -0
- package/dist/templates/showcase/pages/navigation/controller.ts +65 -0
- package/dist/templates/showcase/pages/navigation/state.ts +17 -0
- package/dist/templates/showcase/pages/navigation/view.dml +34 -0
- package/dist/templates/showcase/pages/network/controller.ts +151 -0
- package/dist/templates/showcase/pages/network/state.ts +22 -0
- package/dist/templates/showcase/pages/network/view.dml +67 -0
- package/dist/templates/showcase/pages/payments/controller.ts +46 -0
- package/dist/templates/showcase/pages/payments/state.ts +13 -0
- package/dist/templates/showcase/pages/payments/view.dml +45 -0
- 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/controller.ts +22 -0
- package/dist/templates/showcase/pages/state/state.ts +10 -0
- package/dist/templates/showcase/pages/state/view.dml +33 -0
- package/dist/templates/showcase/pages/text/controller.ts +10 -0
- package/dist/templates/showcase/pages/text/state.ts +8 -0
- package/dist/templates/showcase/pages/text/view.dml +58 -0
- package/dist/templates/showcase/statics/logo.png +0 -0
- package/dist/templates/starter/.env +3 -0
- package/dist/templates/starter/dot.ts +3 -0
- package/dist/templates/starter/pages/index/controller.ts +9 -0
- package/dist/templates/starter/pages/index/state.ts +4 -0
- package/dist/templates/starter/pages/index/view.dml +16 -0
- package/dist/templates/starter/statics/logo.png +0 -0
- package/package.json +53 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Files</Text>
|
|
3
|
+
<Text>Each dot has its own folder. Other dots can't see it. Files stay after the dot closes.</Text>
|
|
4
|
+
|
|
5
|
+
<Text variant="caption" tone="muted">A setlist. Each song is a text file.</Text>
|
|
6
|
+
<View variant="card" gap="small">
|
|
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>
|
|
12
|
+
</View>
|
|
13
|
+
<For each="{{ songs }}" item="song" key="path">
|
|
14
|
+
<View gap="none">
|
|
15
|
+
<Text>{{ song.text }}</Text>
|
|
16
|
+
<Text variant="caption" tone="muted">{{ song.path }}</Text>
|
|
17
|
+
</View>
|
|
18
|
+
</For>
|
|
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>
|
|
21
|
+
</If>
|
|
22
|
+
</View>
|
|
23
|
+
|
|
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>
|
|
39
|
+
<View variant="card" gap="small">
|
|
40
|
+
<If condition="{{ picture }}">
|
|
41
|
+
<Image src="{{ picture }}" alt="Kept artwork" width="64" height="64" />
|
|
42
|
+
<Button variant="secondary" onClick="forgetPicture">Forget it</Button>
|
|
43
|
+
</If>
|
|
44
|
+
<Else>
|
|
45
|
+
<Text variant="caption" tone="muted">None yet. On the Network page, search, download the artwork and tap Keep.</Text>
|
|
46
|
+
</Else>
|
|
47
|
+
</View>
|
|
48
|
+
|
|
49
|
+
<Text variant="caption" tone="muted">{{ usage }}</Text>
|
|
50
|
+
</View>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Release } from './state';
|
|
2
|
+
import state from './state';
|
|
3
|
+
|
|
4
|
+
type Chosen = DotEvent<{ value: string }>;
|
|
5
|
+
|
|
6
|
+
export default definePage({
|
|
7
|
+
initialState: state,
|
|
8
|
+
mutations: {
|
|
9
|
+
chooseTab: (state, event: Chosen) => {
|
|
10
|
+
const tab = event.detail.value;
|
|
11
|
+
return { tab, shown: filter(state.releases, tab, state.genre) };
|
|
12
|
+
},
|
|
13
|
+
chooseGenre: (state, event: DotEvent<unknown, { genre: string }>) => {
|
|
14
|
+
const { genre } = event.data;
|
|
15
|
+
return { genre, shown: filter(state.releases, state.tab, genre) };
|
|
16
|
+
},
|
|
17
|
+
clearFilters: (state) => ({ tab: 'all', genre: 'All', shown: state.releases }),
|
|
18
|
+
chooseWhere: (_state, event: Chosen) => ({ where: event.detail.value }),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
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),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type Kind = 'album' | 'song';
|
|
2
|
+
export type Genre = 'Kriofusion' | 'Afropop' | 'Hip hop' | 'R&B' | 'Palm-wine' | 'Gospel' | 'Bubu';
|
|
3
|
+
|
|
4
|
+
export interface Release {
|
|
5
|
+
id: string;
|
|
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;
|
|
17
|
+
date: 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 };
|
|
30
|
+
}
|
|
31
|
+
|
|
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'),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export default {
|
|
62
|
+
releases,
|
|
63
|
+
// The releases the tab and the chosen genre leave.
|
|
64
|
+
shown: releases,
|
|
65
|
+
tab: 'all',
|
|
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[],
|
|
118
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Tabs and filters</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
|
+
|
|
5
|
+
<Tabs value="{{ tab }}" onChange="chooseTab">
|
|
6
|
+
<Tab value="all" label="All" />
|
|
7
|
+
<Tab value="album" label="Albums" badge="{{ albums }}" />
|
|
8
|
+
<Tab value="song" label="Songs" badge="{{ songs }}" />
|
|
9
|
+
</Tabs>
|
|
10
|
+
|
|
11
|
+
<View direction="row" gap="small" wrap>
|
|
12
|
+
<For each="{{ genres }}" item="option" key="*this">
|
|
13
|
+
<Chip label="{{ option }}" selected="{{ option === genre }}" data-genre="{{ option }}" onTap="chooseGenre" />
|
|
14
|
+
</For>
|
|
15
|
+
</View>
|
|
16
|
+
|
|
17
|
+
<If condition="{{ shown.length > 0 }}">
|
|
18
|
+
<List>
|
|
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
|
+
</ListItem>
|
|
23
|
+
</For>
|
|
24
|
+
</List>
|
|
25
|
+
</If>
|
|
26
|
+
<Else>
|
|
27
|
+
<EmptyState icon="search" title="Nothing here" text="No release matches this tab and genre.">
|
|
28
|
+
<Button variant="secondary" onClick="clearFilters">Clear filters</Button>
|
|
29
|
+
</EmptyState>
|
|
30
|
+
</Else>
|
|
31
|
+
|
|
32
|
+
<View gap="small">
|
|
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" />
|
|
37
|
+
</Tabs>
|
|
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>
|
|
43
|
+
</View>
|
|
44
|
+
</View>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { TicketType } from './state';
|
|
2
|
+
import state, { SHOWS, TICKETS } from './state';
|
|
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
|
+
}
|
|
11
|
+
type Toggled = DotEvent<{ checked: boolean }>;
|
|
12
|
+
|
|
13
|
+
export default definePage({
|
|
14
|
+
initialState: state,
|
|
15
|
+
mutations: {
|
|
16
|
+
setShow: (_state, event: Typed) => ({
|
|
17
|
+
show: event.detail.value,
|
|
18
|
+
showName: SHOWS[event.detail.value] ?? 'Not chosen yet',
|
|
19
|
+
}),
|
|
20
|
+
setName: (_state, event: Typed) => ({ name: event.detail.value }),
|
|
21
|
+
setEmail: (_state, event: Typed) => ({ email: event.detail.value }),
|
|
22
|
+
setPhone: (_state, event: Typed) => ({ phone: event.detail.value }),
|
|
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 }),
|
|
34
|
+
setAgreed: (_state, event: Toggled) => ({ agreed: event.detail.checked }),
|
|
35
|
+
},
|
|
36
|
+
actions: {
|
|
37
|
+
async send(state) {
|
|
38
|
+
const count = Number(state.tickets);
|
|
39
|
+
const errors = {
|
|
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
|
+
: '',
|
|
46
|
+
};
|
|
47
|
+
this.setState({ errors, sent: '', failure: '' });
|
|
48
|
+
if (Object.values(errors).some(Boolean)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.setState({ sending: true });
|
|
52
|
+
try {
|
|
53
|
+
const { show, name, email, phone, shoutOut, ticket, eTickets } = state;
|
|
54
|
+
await this.dash.network.http.request({
|
|
55
|
+
url: 'https://httpbin.org/post',
|
|
56
|
+
method: 'POST',
|
|
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}`,
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
this.setState({ failure: (error as Error).message });
|
|
64
|
+
}
|
|
65
|
+
this.setState({ sending: false });
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
|
|
16
|
+
export default {
|
|
17
|
+
show: '',
|
|
18
|
+
// The order, as the summary shows it.
|
|
19
|
+
showName: 'Not chosen yet',
|
|
20
|
+
ticketName: TICKETS.regular.name,
|
|
21
|
+
total: 'Le 0',
|
|
22
|
+
name: '',
|
|
23
|
+
email: '',
|
|
24
|
+
phone: '',
|
|
25
|
+
tickets: '',
|
|
26
|
+
shoutOut: '',
|
|
27
|
+
ticket: 'regular' as TicketType,
|
|
28
|
+
eTickets: true,
|
|
29
|
+
agreed: false,
|
|
30
|
+
errors: { show: '', phone: '', tickets: '', shoutOut: '' },
|
|
31
|
+
sending: false,
|
|
32
|
+
sent: '',
|
|
33
|
+
failure: '',
|
|
34
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Forms</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
|
+
|
|
5
|
+
<View variant="card">
|
|
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
|
+
</Select>
|
|
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
|
+
</View>
|
|
17
|
+
|
|
18
|
+
<View variant="card">
|
|
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
|
+
</RadioGroup>
|
|
24
|
+
<Divider />
|
|
25
|
+
<Switch label="Email me the e-tickets" checked="{{ eTickets }}" onChange="setETickets" />
|
|
26
|
+
<Checkbox label="I agree to the terms" checked="{{ agreed }}" onChange="setAgreed" />
|
|
27
|
+
</View>
|
|
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
|
+
|
|
39
|
+
<If condition="{{ sent }}">
|
|
40
|
+
<Notice tone="success" title="Booked">{{ sent }}</Notice>
|
|
41
|
+
</If>
|
|
42
|
+
<If condition="{{ failure }}">
|
|
43
|
+
<Notice tone="danger" title="Not booked">{{ failure }}</Notice>
|
|
44
|
+
</If>
|
|
45
|
+
|
|
46
|
+
<Footer>
|
|
47
|
+
<Button onClick="send" loading="{{ sending }}" disabled="{{ !agreed }}">Buy tickets</Button>
|
|
48
|
+
</Footer>
|
|
49
|
+
</View>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import state from './state';
|
|
2
|
+
|
|
3
|
+
// AUDACITY's id in Apple's catalog; its cover's address can change, so the page looks it up.
|
|
4
|
+
const AUDACITY = 6811765244;
|
|
5
|
+
|
|
6
|
+
interface Lookup {
|
|
7
|
+
results: { artworkUrl100: string }[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default definePage({
|
|
11
|
+
initialState: state,
|
|
12
|
+
hooks: {
|
|
13
|
+
async onLoad() {
|
|
14
|
+
try {
|
|
15
|
+
const { body } = await this.dash.network.http.request<Lookup>({
|
|
16
|
+
url: `https://itunes.apple.com/lookup?id=${AUDACITY}`,
|
|
17
|
+
as: 'json',
|
|
18
|
+
});
|
|
19
|
+
const artwork = body.results[0]?.artworkUrl100;
|
|
20
|
+
// The catalog's artwork comes in any size its address names.
|
|
21
|
+
this.setState(
|
|
22
|
+
artwork
|
|
23
|
+
? { cover: artwork.replace('100x100bb', '600x600bb') }
|
|
24
|
+
: { coverNote: "Apple's catalog has no cover for it" },
|
|
25
|
+
);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
this.setState({ coverNote: (error as Error).message });
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
mutations: {
|
|
32
|
+
grow: (state) => ({ size: Math.min(state.size + 16, 128) }),
|
|
33
|
+
shrink: (state) => ({ size: Math.max(state.size - 16, 16) }),
|
|
34
|
+
logoLoaded: (_state, event: DotEvent<{ width: number; height: number }>) => ({
|
|
35
|
+
loaded: `${event.detail.width} × ${event.detail.height}`,
|
|
36
|
+
}),
|
|
37
|
+
missingFailed: (_state, event: DotEvent<{ message: string }>) => ({
|
|
38
|
+
failure: event.detail.message,
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
size: 64,
|
|
3
|
+
loaded: '',
|
|
4
|
+
// Built at run time, so the build doesn't check that the file exists.
|
|
5
|
+
missing: 'statics/missing.png',
|
|
6
|
+
failure: '',
|
|
7
|
+
// The cover of Drizilik's album AUDACITY, which the page asks Apple's catalog for.
|
|
8
|
+
cover: '',
|
|
9
|
+
coverNote: '',
|
|
10
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<Text variant="title">Images</Text>
|
|
3
|
+
<Text>Pictures in the statics/ folder come with the dot, and the build checks each one. Pictures from the web load from their address.</Text>
|
|
4
|
+
|
|
5
|
+
<View variant="card" direction="row" align="end" gap="large">
|
|
6
|
+
<Image src="statics/logo.png" alt="Small logo" width="24" height="24" />
|
|
7
|
+
<Image src="statics/logo.png" alt="Medium logo" width="48" height="48" />
|
|
8
|
+
<Image src="statics/logo.png" alt="Large logo" width="96" height="96" />
|
|
9
|
+
</View>
|
|
10
|
+
|
|
11
|
+
<Text variant="caption" tone="muted">From the web: the cover of Drizilik's album AUDACITY. With aspect="1:1" and no width, it fills the card.</Text>
|
|
12
|
+
<View variant="card" gap="small">
|
|
13
|
+
<If condition="{{ cover }}">
|
|
14
|
+
<Image src="{{ cover }}" alt="The cover of AUDACITY, by Drizilik" aspect="1:1" />
|
|
15
|
+
<Text weight="medium">AUDACITY</Text>
|
|
16
|
+
<Text variant="caption" tone="muted">Drizilik raps mostly in Krio, in a style called Kriofusion. He won Best Male Artist at the 2026 SLeDU awards.</Text>
|
|
17
|
+
</If>
|
|
18
|
+
<ElseIf condition="{{ coverNote }}">
|
|
19
|
+
<Text variant="caption" tone="muted">{{ coverNote }}</Text>
|
|
20
|
+
</ElseIf>
|
|
21
|
+
<Else>
|
|
22
|
+
<Skeleton shape="box" height="280" />
|
|
23
|
+
</Else>
|
|
24
|
+
</View>
|
|
25
|
+
|
|
26
|
+
<Text variant="caption" tone="muted">Shapes, and how a picture fills its space</Text>
|
|
27
|
+
<View variant="card" direction="row" align="center" gap="large">
|
|
28
|
+
<Image src="statics/logo.png" alt="Rounded" width="56" height="56" />
|
|
29
|
+
<Image src="statics/logo.png" alt="Square" width="56" height="56" shape="square" />
|
|
30
|
+
<Image src="statics/logo.png" alt="Circle" width="56" height="56" shape="circle" />
|
|
31
|
+
<Image src="statics/logo.png" alt="Contained" width="96" height="56" fit="contain" />
|
|
32
|
+
</View>
|
|
33
|
+
|
|
34
|
+
<Text variant="caption" tone="muted">Sizes can come from state</Text>
|
|
35
|
+
<View variant="card" gap="small">
|
|
36
|
+
<Image src="statics/logo.png" alt="Logo" width="{{ size }}" height="{{ size }}" onLoad="logoLoaded" />
|
|
37
|
+
<Text>{{ size }} × {{ size }} on screen, {{ loaded }} in the file</Text>
|
|
38
|
+
<View direction="row" gap="small">
|
|
39
|
+
<Button variant="secondary" onClick="shrink" disabled="{{ size <= 16 }}">Smaller</Button>
|
|
40
|
+
<Button variant="secondary" onClick="grow" disabled="{{ size >= 128 }}">Bigger</Button>
|
|
41
|
+
</View>
|
|
42
|
+
</View>
|
|
43
|
+
|
|
44
|
+
<Text variant="caption" tone="muted">A broken image shows why it failed</Text>
|
|
45
|
+
<View variant="card">
|
|
46
|
+
<If condition="{{ failure }}">
|
|
47
|
+
<Text>{{ failure }}</Text>
|
|
48
|
+
</If>
|
|
49
|
+
<Else>
|
|
50
|
+
<Image src="{{ missing }}" alt="" width="48" height="48" onError="missingFailed" />
|
|
51
|
+
</Else>
|
|
52
|
+
</View>
|
|
53
|
+
</View>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export interface Feature {
|
|
2
|
+
page: PageId;
|
|
3
|
+
title: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
icon: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Section {
|
|
9
|
+
title: string;
|
|
10
|
+
features: Feature[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
title: 'Salone Sounds',
|
|
15
|
+
sections: [
|
|
16
|
+
{
|
|
17
|
+
title: 'Views',
|
|
18
|
+
features: [
|
|
19
|
+
{
|
|
20
|
+
page: 'layout',
|
|
21
|
+
title: 'Layout',
|
|
22
|
+
summary: 'Rows, columns, cards, a shelf to scroll and a grid',
|
|
23
|
+
icon: 'grid',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
page: 'text',
|
|
27
|
+
title: 'Text and bindings',
|
|
28
|
+
summary: 'Text styles, tones, links and bindings',
|
|
29
|
+
icon: 'edit',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
page: 'images',
|
|
33
|
+
title: 'Images',
|
|
34
|
+
summary: 'Pictures from the dot and from the web',
|
|
35
|
+
icon: 'image',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
page: 'carousel',
|
|
39
|
+
title: 'Carousel',
|
|
40
|
+
summary: 'Slides of Gen Z artists and Salone history',
|
|
41
|
+
icon: 'image',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
page: 'lists',
|
|
45
|
+
title: 'Lists and icons',
|
|
46
|
+
summary: 'Rows to tap, badges and icons',
|
|
47
|
+
icon: 'list',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
page: 'filters',
|
|
51
|
+
title: 'Tabs and filters',
|
|
52
|
+
summary: 'Tabs, chips and empty states',
|
|
53
|
+
icon: 'search',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
page: 'forms',
|
|
57
|
+
title: 'Forms',
|
|
58
|
+
summary: 'Fields, choices and switches for buying tickets',
|
|
59
|
+
icon: 'check',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
page: 'payments',
|
|
63
|
+
title: 'Payments',
|
|
64
|
+
summary: 'Amounts, QR codes and one-time codes',
|
|
65
|
+
icon: 'cash',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
page: 'feedback',
|
|
69
|
+
title: 'Feedback',
|
|
70
|
+
summary: 'Loading, toasts, progress and notices',
|
|
71
|
+
icon: 'bell',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: 'Logic',
|
|
77
|
+
features: [
|
|
78
|
+
{
|
|
79
|
+
page: 'state',
|
|
80
|
+
title: 'State, mutations and actions',
|
|
81
|
+
summary: 'State, conditions and lists',
|
|
82
|
+
icon: 'refresh',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
page: 'navigation',
|
|
86
|
+
title: 'Navigation and hooks',
|
|
87
|
+
summary: 'Opening pages, going back, and page hooks',
|
|
88
|
+
icon: 'arrow-right',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
title: 'Dash',
|
|
94
|
+
features: [
|
|
95
|
+
{
|
|
96
|
+
page: 'device',
|
|
97
|
+
title: 'Alerts and device',
|
|
98
|
+
summary: 'Alerts, copy, share, links and the torch',
|
|
99
|
+
icon: 'mobile',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
page: 'network',
|
|
103
|
+
title: 'Network',
|
|
104
|
+
summary: 'Requests, JSON, downloads and uploads',
|
|
105
|
+
icon: 'wifi',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
page: 'files',
|
|
109
|
+
title: 'Files',
|
|
110
|
+
summary: "Saving files in the dot's own folder",
|
|
111
|
+
icon: 'file',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
page: 'cache',
|
|
115
|
+
title: 'Cache',
|
|
116
|
+
summary: 'Values that expire after a while',
|
|
117
|
+
icon: 'refresh',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
page: 'secrets',
|
|
121
|
+
title: 'Secrets',
|
|
122
|
+
summary: 'Sign-in tokens kept encrypted',
|
|
123
|
+
icon: 'lock',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
] as Section[],
|
|
128
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<View>
|
|
2
|
+
<View direction="row" align="center" gap="large">
|
|
3
|
+
<Image src="statics/logo.png" alt="Salone Sounds logo" width="48" height="48" />
|
|
4
|
+
<View gap="none">
|
|
5
|
+
<Text variant="title">{{ title }}</Text>
|
|
6
|
+
<Text variant="caption" tone="muted">What a dot can do, shown with Sierra Leone's music</Text>
|
|
7
|
+
</View>
|
|
8
|
+
</View>
|
|
9
|
+
|
|
10
|
+
<For each="{{ sections }}" item="section" key="title">
|
|
11
|
+
<View gap="small">
|
|
12
|
+
<Text variant="caption" tone="muted">{{ section.title }}</Text>
|
|
13
|
+
<List>
|
|
14
|
+
<For each="{{ section.features }}" item="feature" key="page">
|
|
15
|
+
<ListItem icon="{{ feature.icon }}" title="{{ feature.title }}" subtitle="{{ feature.summary }}" data-page="{{ feature.page }}" onTap="open" />
|
|
16
|
+
</For>
|
|
17
|
+
</List>
|
|
18
|
+
</View>
|
|
19
|
+
</For>
|
|
20
|
+
</View>
|