@jtakeit/kit 0.3.0 → 0.5.1
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 +36 -0
- package/bin/jtkit.mjs +44 -0
- package/booking.css +55 -1
- package/elements/booking-ui.js +371 -0
- package/elements/booking.js +173 -8
- package/elements/writing.js +193 -0
- package/lib/booking/parts.mjs +63 -0
- package/lib/booking/render.mjs +16 -4
- package/lib/booking/words.mjs +200 -0
- package/lib/catalogue.mjs +18 -2
- package/lib/money.mjs +24 -4
- package/lib/phases.mjs +139 -0
- package/lib/progress.mjs +317 -0
- package/package.json +21 -2
- package/types/booking-ui.d.ts +6 -0
- package/types/booking.d.ts +6 -0
- package/types/parts.d.ts +7 -0
- package/types/phases.d.ts +41 -0
- package/types/progress.d.ts +50 -0
package/lib/booking/words.mjs
CHANGED
|
@@ -14,6 +14,31 @@ export const LOCALES = Object.freeze(['uk', 'de', 'en', 'ru', 'es', 'it', 'pt',
|
|
|
14
14
|
|
|
15
15
|
const BASE = {
|
|
16
16
|
uk: {
|
|
17
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
18
|
+
// kept time, the link kept (10 October 2026)
|
|
19
|
+
notWith: 'не з {who}',
|
|
20
|
+
retime: 'Візит тепер іншої тривалості, тому час змінився — оберіть ще раз.',
|
|
21
|
+
until: 'Приблизно до {time}',
|
|
22
|
+
withNamed: 'Майстер: {who}',
|
|
23
|
+
keep: 'Збережіть це посилання — ним можна перенести чи скасувати запис.',
|
|
24
|
+
copy: 'Копіювати',
|
|
25
|
+
copied: 'Скопійовано',
|
|
26
|
+
open: 'Відкрити',
|
|
27
|
+
stepWhat: 'Що і коли',
|
|
28
|
+
stepWho: 'Хто прийде',
|
|
29
|
+
next: 'Далі',
|
|
30
|
+
change: 'Змінити',
|
|
31
|
+
needAll: 'Спершу оберіть послугу, день і час.',
|
|
32
|
+
summary: 'Ваш запис',
|
|
33
|
+
what: 'Що',
|
|
34
|
+
when: 'Коли',
|
|
35
|
+
takes: 'Тривалість',
|
|
36
|
+
morning: 'Ранок',
|
|
37
|
+
afternoon: 'День',
|
|
38
|
+
evening: 'Вечір',
|
|
39
|
+
prevWeek: 'Попередній тиждень',
|
|
40
|
+
nextWeek: 'Наступний тиждень',
|
|
41
|
+
again: 'Записатися ще раз',
|
|
17
42
|
service: 'Послуга', services: 'Послуги', total: 'Разом', pickService: 'Оберіть хоча б одну послугу.', resource: 'Майстер', any: 'Будь-хто', day: 'День', time: 'Час',
|
|
18
43
|
name: "Ім'я", phone: 'Телефон', email: 'Електронна пошта', note: 'Коментар',
|
|
19
44
|
book: 'Записатися', sending: 'Записуємо…', none: 'У цей день вільного часу немає.', noParty: 'Немає вільних місць для такої компанії.',
|
|
@@ -27,6 +52,31 @@ const BASE = {
|
|
|
27
52
|
needContact: 'Вкажіть телефон або пошту, щоб ми могли з вами звʼязатися.', noScript: 'Онлайн-форма працює з увімкненим JavaScript — увімкніть його або зателефонуйте нам.',
|
|
28
53
|
},
|
|
29
54
|
de: {
|
|
55
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
56
|
+
// kept time, the link kept (10 October 2026)
|
|
57
|
+
notWith: 'nicht bei {who}',
|
|
58
|
+
retime: 'Der Besuch dauert jetzt anders lang, darum haben sich die Zeiten verschoben — bitte wählen Sie neu.',
|
|
59
|
+
until: 'Bis etwa {time}',
|
|
60
|
+
withNamed: 'Bei {who}',
|
|
61
|
+
keep: 'Bewahren Sie diesen Link auf — damit verschieben oder stornieren Sie den Termin.',
|
|
62
|
+
copy: 'Kopieren',
|
|
63
|
+
copied: 'Kopiert',
|
|
64
|
+
open: 'Öffnen',
|
|
65
|
+
stepWhat: 'Was und wann',
|
|
66
|
+
stepWho: 'Wer kommt',
|
|
67
|
+
next: 'Weiter',
|
|
68
|
+
change: 'Ändern',
|
|
69
|
+
needAll: 'Bitte zuerst Leistung, Tag und Zeit wählen.',
|
|
70
|
+
summary: 'Ihr Termin',
|
|
71
|
+
what: 'Was',
|
|
72
|
+
when: 'Wann',
|
|
73
|
+
takes: 'Dauer',
|
|
74
|
+
morning: 'Vormittag',
|
|
75
|
+
afternoon: 'Nachmittag',
|
|
76
|
+
evening: 'Abend',
|
|
77
|
+
prevWeek: 'Vorherige Woche',
|
|
78
|
+
nextWeek: 'Nächste Woche',
|
|
79
|
+
again: 'Noch einen Termin buchen',
|
|
30
80
|
service: 'Leistung', services: 'Leistungen', total: 'Zusammen', pickService: 'Bitte mindestens eine Leistung wählen.', resource: 'Bei', any: 'Egal wer', day: 'Tag', time: 'Uhrzeit',
|
|
31
81
|
name: 'Name', phone: 'Telefon', email: 'E-Mail', note: 'Bemerkung',
|
|
32
82
|
book: 'Termin buchen', sending: 'Wird gebucht…', none: 'An diesem Tag ist nichts frei.', noParty: 'Für so viele Personen gibt es keinen freien Platz.',
|
|
@@ -40,6 +90,31 @@ const BASE = {
|
|
|
40
90
|
needContact: 'Bitte Telefon oder E-Mail angeben, damit wir Sie erreichen können.', noScript: 'Für die Online-Buchung ist JavaScript nötig — oder rufen Sie uns an.',
|
|
41
91
|
},
|
|
42
92
|
en: {
|
|
93
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
94
|
+
// kept time, the link kept (10 October 2026)
|
|
95
|
+
notWith: 'not with {who}',
|
|
96
|
+
retime: 'The visit is a different length now, so the times moved — pick one again.',
|
|
97
|
+
until: 'Until about {time}',
|
|
98
|
+
withNamed: 'With {who}',
|
|
99
|
+
keep: 'Keep this link — it is how you move or cancel the booking.',
|
|
100
|
+
copy: 'Copy',
|
|
101
|
+
copied: 'Copied',
|
|
102
|
+
open: 'Open',
|
|
103
|
+
stepWhat: 'What and when',
|
|
104
|
+
stepWho: 'Who is coming',
|
|
105
|
+
next: 'Continue',
|
|
106
|
+
change: 'Change',
|
|
107
|
+
needAll: 'Pick a service, a day and a time first.',
|
|
108
|
+
summary: 'Your booking',
|
|
109
|
+
what: 'What',
|
|
110
|
+
when: 'When',
|
|
111
|
+
takes: 'Takes',
|
|
112
|
+
morning: 'Morning',
|
|
113
|
+
afternoon: 'Afternoon',
|
|
114
|
+
evening: 'Evening',
|
|
115
|
+
prevWeek: 'Previous week',
|
|
116
|
+
nextWeek: 'Next week',
|
|
117
|
+
again: 'Book another time',
|
|
43
118
|
service: 'Service', services: 'Services', total: 'Total', pickService: 'Pick at least one service.', resource: 'With', any: 'Anyone', day: 'Day', time: 'Time',
|
|
44
119
|
name: 'Name', phone: 'Phone', email: 'Email', note: 'Note',
|
|
45
120
|
book: 'Book', sending: 'Booking…', none: 'Nothing is free on that day.', noParty: 'There is no place for a party this large.',
|
|
@@ -53,6 +128,31 @@ const BASE = {
|
|
|
53
128
|
needContact: 'Leave a phone number or an email so we can reach you.', noScript: 'Booking online needs JavaScript — or ring us.',
|
|
54
129
|
},
|
|
55
130
|
ru: {
|
|
131
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
132
|
+
// kept time, the link kept (10 October 2026)
|
|
133
|
+
notWith: 'не у {who}',
|
|
134
|
+
retime: 'Визит теперь другой длительности, поэтому время изменилось — выберите ещё раз.',
|
|
135
|
+
until: 'Примерно до {time}',
|
|
136
|
+
withNamed: 'Мастер: {who}',
|
|
137
|
+
keep: 'Сохраните эту ссылку — по ней можно перенести или отменить запись.',
|
|
138
|
+
copy: 'Копировать',
|
|
139
|
+
copied: 'Скопировано',
|
|
140
|
+
open: 'Открыть',
|
|
141
|
+
stepWhat: 'Что и когда',
|
|
142
|
+
stepWho: 'Кто придёт',
|
|
143
|
+
next: 'Далее',
|
|
144
|
+
change: 'Изменить',
|
|
145
|
+
needAll: 'Сначала выберите услугу, день и время.',
|
|
146
|
+
summary: 'Ваша запись',
|
|
147
|
+
what: 'Что',
|
|
148
|
+
when: 'Когда',
|
|
149
|
+
takes: 'Длительность',
|
|
150
|
+
morning: 'Утро',
|
|
151
|
+
afternoon: 'День',
|
|
152
|
+
evening: 'Вечер',
|
|
153
|
+
prevWeek: 'Предыдущая неделя',
|
|
154
|
+
nextWeek: 'Следующая неделя',
|
|
155
|
+
again: 'Записаться ещё раз',
|
|
56
156
|
service: "Услуга",
|
|
57
157
|
services: "Услуги",
|
|
58
158
|
total: "Итого",
|
|
@@ -96,6 +196,31 @@ const BASE = {
|
|
|
96
196
|
noScript: "Онлайн-форма работает с включенным JavaScript — включите его или позвоните нам.",
|
|
97
197
|
},
|
|
98
198
|
es: {
|
|
199
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
200
|
+
// kept time, the link kept (10 October 2026)
|
|
201
|
+
notWith: 'no con {who}',
|
|
202
|
+
retime: 'La visita ahora dura distinto, así que los horarios cambiaron: elija uno de nuevo.',
|
|
203
|
+
until: 'Hasta las {time} aproximadamente',
|
|
204
|
+
withNamed: 'Con {who}',
|
|
205
|
+
keep: 'Guarde este enlace: con él puede mover o cancelar la reserva.',
|
|
206
|
+
copy: 'Copiar',
|
|
207
|
+
copied: 'Copiado',
|
|
208
|
+
open: 'Abrir',
|
|
209
|
+
stepWhat: 'Qué y cuándo',
|
|
210
|
+
stepWho: 'Quién viene',
|
|
211
|
+
next: 'Continuar',
|
|
212
|
+
change: 'Cambiar',
|
|
213
|
+
needAll: 'Elija primero un servicio, un día y una hora.',
|
|
214
|
+
summary: 'Su reserva',
|
|
215
|
+
what: 'Qué',
|
|
216
|
+
when: 'Cuándo',
|
|
217
|
+
takes: 'Duración',
|
|
218
|
+
morning: 'Mañana',
|
|
219
|
+
afternoon: 'Tarde',
|
|
220
|
+
evening: 'Noche',
|
|
221
|
+
prevWeek: 'Semana anterior',
|
|
222
|
+
nextWeek: 'Semana siguiente',
|
|
223
|
+
again: 'Reservar otra hora',
|
|
99
224
|
service: "Servicio",
|
|
100
225
|
services: "Servicios",
|
|
101
226
|
total: "Total",
|
|
@@ -139,6 +264,31 @@ const BASE = {
|
|
|
139
264
|
noScript: "Para reservar en línea hace falta JavaScript — o llámenos.",
|
|
140
265
|
},
|
|
141
266
|
it: {
|
|
267
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
268
|
+
// kept time, the link kept (10 October 2026)
|
|
269
|
+
notWith: 'non con {who}',
|
|
270
|
+
retime: 'La visita ora ha una durata diversa, quindi gli orari sono cambiati: ne scelga uno di nuovo.',
|
|
271
|
+
until: 'Fino alle {time} circa',
|
|
272
|
+
withNamed: 'Con {who}',
|
|
273
|
+
keep: 'Conservi questo link: serve per spostare o annullare la prenotazione.',
|
|
274
|
+
copy: 'Copia',
|
|
275
|
+
copied: 'Copiato',
|
|
276
|
+
open: 'Apri',
|
|
277
|
+
stepWhat: 'Cosa e quando',
|
|
278
|
+
stepWho: 'Chi viene',
|
|
279
|
+
next: 'Continua',
|
|
280
|
+
change: 'Cambia',
|
|
281
|
+
needAll: 'Scelga prima un servizio, un giorno e un orario.',
|
|
282
|
+
summary: 'La sua prenotazione',
|
|
283
|
+
what: 'Cosa',
|
|
284
|
+
when: 'Quando',
|
|
285
|
+
takes: 'Durata',
|
|
286
|
+
morning: 'Mattina',
|
|
287
|
+
afternoon: 'Pomeriggio',
|
|
288
|
+
evening: 'Sera',
|
|
289
|
+
prevWeek: 'Settimana precedente',
|
|
290
|
+
nextWeek: 'Settimana successiva',
|
|
291
|
+
again: 'Prenota un altro orario',
|
|
142
292
|
service: "Servizio",
|
|
143
293
|
services: "Servizi",
|
|
144
294
|
total: "Totale",
|
|
@@ -182,6 +332,31 @@ const BASE = {
|
|
|
182
332
|
noScript: "Per prenotare online serve JavaScript: oppure ci chiami.",
|
|
183
333
|
},
|
|
184
334
|
pt: {
|
|
335
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
336
|
+
// kept time, the link kept (10 October 2026)
|
|
337
|
+
notWith: 'não com {who}',
|
|
338
|
+
retime: 'A visita tem agora outra duração, por isso os horários mudaram — escolha de novo.',
|
|
339
|
+
until: 'Até cerca das {time}',
|
|
340
|
+
withNamed: 'Com {who}',
|
|
341
|
+
keep: 'Guarde esta ligação — é com ela que move ou cancela a marcação.',
|
|
342
|
+
copy: 'Copiar',
|
|
343
|
+
copied: 'Copiado',
|
|
344
|
+
open: 'Abrir',
|
|
345
|
+
stepWhat: 'O quê e quando',
|
|
346
|
+
stepWho: 'Quem vem',
|
|
347
|
+
next: 'Continuar',
|
|
348
|
+
change: 'Alterar',
|
|
349
|
+
needAll: 'Escolha primeiro um serviço, um dia e uma hora.',
|
|
350
|
+
summary: 'A sua marcação',
|
|
351
|
+
what: 'O quê',
|
|
352
|
+
when: 'Quando',
|
|
353
|
+
takes: 'Duração',
|
|
354
|
+
morning: 'Manhã',
|
|
355
|
+
afternoon: 'Tarde',
|
|
356
|
+
evening: 'Noite',
|
|
357
|
+
prevWeek: 'Semana anterior',
|
|
358
|
+
nextWeek: 'Semana seguinte',
|
|
359
|
+
again: 'Marcar outra hora',
|
|
185
360
|
service: "Serviço",
|
|
186
361
|
services: "Serviços",
|
|
187
362
|
total: "Total",
|
|
@@ -225,6 +400,31 @@ const BASE = {
|
|
|
225
400
|
noScript: "Reservar online precisa de JavaScript — ou ligue-nos.",
|
|
226
401
|
},
|
|
227
402
|
fr: {
|
|
403
|
+
// the form's look — the steps, the week, the parts of the day, the
|
|
404
|
+
// kept time, the link kept (10 October 2026)
|
|
405
|
+
notWith: 'pas avec {who}',
|
|
406
|
+
retime: 'La visite n’a plus la même durée, les horaires ont donc changé — choisissez-en un à nouveau.',
|
|
407
|
+
until: 'Jusqu’à {time} environ',
|
|
408
|
+
withNamed: 'Avec {who}',
|
|
409
|
+
keep: 'Conservez ce lien : c’est avec lui que vous déplacez ou annulez la réservation.',
|
|
410
|
+
copy: 'Copier',
|
|
411
|
+
copied: 'Copié',
|
|
412
|
+
open: 'Ouvrir',
|
|
413
|
+
stepWhat: 'Quoi et quand',
|
|
414
|
+
stepWho: 'Qui vient',
|
|
415
|
+
next: 'Continuer',
|
|
416
|
+
change: 'Modifier',
|
|
417
|
+
needAll: 'Choisissez d’abord une prestation, un jour et une heure.',
|
|
418
|
+
summary: 'Votre réservation',
|
|
419
|
+
what: 'Quoi',
|
|
420
|
+
when: 'Quand',
|
|
421
|
+
takes: 'Durée',
|
|
422
|
+
morning: 'Matin',
|
|
423
|
+
afternoon: 'Après-midi',
|
|
424
|
+
evening: 'Soir',
|
|
425
|
+
prevWeek: 'Semaine précédente',
|
|
426
|
+
nextWeek: 'Semaine suivante',
|
|
427
|
+
again: 'Réserver un autre créneau',
|
|
228
428
|
service: "Prestation",
|
|
229
429
|
services: "Prestations",
|
|
230
430
|
total: "Total",
|
package/lib/catalogue.mjs
CHANGED
|
@@ -1346,13 +1346,28 @@ const sharedBlocks = existsSync(sharedFile)
|
|
|
1346
1346
|
const byType = new Map(catalogue.blocks.map((block) => [block.type, block]));
|
|
1347
1347
|
const disagreements = [];
|
|
1348
1348
|
|
|
1349
|
+
/** `/de/services/x` without its language: `/services/x`. The site's own language has no prefix. */
|
|
1350
|
+
function basePathOf(path, locales) {
|
|
1351
|
+
for (const locale of locales) {
|
|
1352
|
+
if (path === `/${locale}` || path === `/${locale}/`) return '/';
|
|
1353
|
+
if (path.startsWith(`/${locale}/`)) return path.slice(locale.length + 1);
|
|
1354
|
+
}
|
|
1355
|
+
return path;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1349
1358
|
for (const page of pages) {
|
|
1350
1359
|
const onThePage = annotationsByPage.get(page.path);
|
|
1351
1360
|
if (onThePage === undefined) {
|
|
1352
1361
|
// A collection with `pages: false` builds no page for its entries: they
|
|
1353
1362
|
// are read by the page that lists them and by the diary.
|
|
1363
|
+
// Said by the entry's own collection, or by its address — with the
|
|
1364
|
+
// language in front of it for an entry in another one: `/de/services/x`
|
|
1365
|
+
// is the `services` collection's as much as `/services/x` is (wiki/75 of
|
|
1366
|
+
// the platform; a German salon's eight services were each reported as a
|
|
1367
|
+
// page the build did not produce, 10 October 2026).
|
|
1368
|
+
const bare = basePathOf(page.path, catalogue.locales ?? []);
|
|
1354
1369
|
const listedOnly = collections.some(
|
|
1355
|
-
(c) => c.pages === false && page.
|
|
1370
|
+
(c) => c.pages === false && (page.collection === c.name || bare.startsWith(`${c.prefix}/`)),
|
|
1356
1371
|
);
|
|
1357
1372
|
if (listedOnly) {
|
|
1358
1373
|
/*
|
|
@@ -1502,8 +1517,9 @@ for (const [where, found] of annotationsByPage) {
|
|
|
1502
1517
|
* against the entry's own type.
|
|
1503
1518
|
*/
|
|
1504
1519
|
const entry = pages.find((one) => one.path === address);
|
|
1520
|
+
// By the entry's collection, or its address without the language.
|
|
1505
1521
|
const listedOnly = entry !== undefined && collections.some(
|
|
1506
|
-
(c) => c.pages === false && address.startsWith(`${c.prefix}/`),
|
|
1522
|
+
(c) => c.pages === false && (entry.collection === c.name || basePathOf(address, catalogue.locales ?? []).startsWith(`${c.prefix}/`)),
|
|
1507
1523
|
);
|
|
1508
1524
|
if (listedOnly) {
|
|
1509
1525
|
const m = /^blocks\[(\d+)\]\.([A-Za-z0-9_]+)/.exec(inner);
|
package/lib/money.mjs
CHANGED
|
@@ -15,16 +15,36 @@
|
|
|
15
15
|
* @returns {string}
|
|
16
16
|
*/
|
|
17
17
|
export function money(minor, currency, locale) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// In the currency's own minor unit (11 October 2026): a hundredth of a
|
|
19
|
+
// franc, a whole yen, a thousandth of a dinar — ISO 4217's digits, which
|
|
20
|
+
// `Intl` carries and the platform counts by too. A hundredth everywhere
|
|
21
|
+
// showed ¥1000 as «¥10».
|
|
22
|
+
const places = currency ? digitsOf(currency, locale) : 2;
|
|
23
|
+
const value = minor / 10 ** places;
|
|
24
|
+
const digits = Number.isInteger(value) ? 0 : places;
|
|
20
25
|
if (!currency) {
|
|
21
|
-
return new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits:
|
|
26
|
+
return new Intl.NumberFormat(locale, { minimumFractionDigits: digits, maximumFractionDigits: places }).format(value);
|
|
22
27
|
}
|
|
23
28
|
return new Intl.NumberFormat(locale, {
|
|
24
29
|
style: 'currency',
|
|
25
30
|
currency: currency.toUpperCase(),
|
|
26
31
|
currencyDisplay: 'narrowSymbol',
|
|
27
32
|
minimumFractionDigits: digits,
|
|
28
|
-
maximumFractionDigits:
|
|
33
|
+
maximumFractionDigits: places,
|
|
29
34
|
}).format(value);
|
|
30
35
|
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* How many digits a currency's minor unit has, by ISO 4217 as `Intl` knows it.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} currency
|
|
41
|
+
* @param {string} [locale]
|
|
42
|
+
* @returns {number}
|
|
43
|
+
*/
|
|
44
|
+
export function digitsOf(currency, locale = 'en') {
|
|
45
|
+
try {
|
|
46
|
+
return new Intl.NumberFormat(locale, { style: 'currency', currency: currency.toUpperCase() }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
47
|
+
} catch {
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/lib/phases.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seven phases of a site being written, from two halves (jtakeit-core,
|
|
3
|
+
* wiki/73 · §4 and §7).
|
|
4
|
+
*
|
|
5
|
+
* The kit's half is `readProgress` — the brief, the layout, the writing,
|
|
6
|
+
* read off the files. The platform's half — the judge's word, the
|
|
7
|
+
* repository, the build, the preview, and the panel's setting up — comes
|
|
8
|
+
* through the dev session (`jtkit session`). This turns the two into the
|
|
9
|
+
* strip the page shows, by the same rules the panel's own card uses
|
|
10
|
+
* (entities/site/writing.ts there): a phase is a fact; a later phase
|
|
11
|
+
* counts only once every phase before it does; written means the kit's
|
|
12
|
+
* still-to-do list is empty, not that the required fields are filled.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const PHASES = ['brief', 'laidOut', 'writing', 'judged', 'attached', 'building', 'preview'];
|
|
16
|
+
|
|
17
|
+
export const PHASE_NAMES = {
|
|
18
|
+
brief: 'Brief',
|
|
19
|
+
laidOut: 'Laid out',
|
|
20
|
+
writing: 'Writing',
|
|
21
|
+
judged: 'Checked',
|
|
22
|
+
attached: 'Attached',
|
|
23
|
+
building: 'Building',
|
|
24
|
+
preview: 'Preview',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** The panel's steps, as the page names them; `agent` ones are not the developer's. */
|
|
28
|
+
export const STEP_NAMES = {
|
|
29
|
+
build: 'The agent builds it',
|
|
30
|
+
diary: 'Bookings: the rules and what is booked',
|
|
31
|
+
business: 'Who owns this site',
|
|
32
|
+
notifications: 'Where enquiries and bookings go',
|
|
33
|
+
subscription: 'The subscription',
|
|
34
|
+
access: 'What the studio may do',
|
|
35
|
+
payee: 'Who gets paid',
|
|
36
|
+
launch: 'First publish',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Where the site is.
|
|
41
|
+
*
|
|
42
|
+
* @param {import('./progress.mjs').Progress | null | undefined} progress the kit's reading, or null where none
|
|
43
|
+
* @param {object | null | undefined} platform the session's answer (`/v1/dev-sessions/{id}/progress`), or null where not connected
|
|
44
|
+
*/
|
|
45
|
+
export function phasesOf(progress, platform) {
|
|
46
|
+
const seen = progress !== null && progress !== undefined;
|
|
47
|
+
const known = platform !== null && platform !== undefined && platform.error === undefined;
|
|
48
|
+
const attached = known && platform.attached === true;
|
|
49
|
+
const served = known && platform.served === true;
|
|
50
|
+
const build = known && platform.build ? platform.build : null;
|
|
51
|
+
|
|
52
|
+
const briefDone = seen
|
|
53
|
+
? progress.brief !== null && progress.brief.facts > 0 && progress.brief.sections['Decisions'] === true
|
|
54
|
+
: attached;
|
|
55
|
+
const laidOutDone = seen ? progress.kit !== null || progress.catalogue === true : attached;
|
|
56
|
+
const writingDone = seen
|
|
57
|
+
? progress.pages.length > 0 &&
|
|
58
|
+
progress.pages.every((one) => one.written) &&
|
|
59
|
+
Object.values(progress.collections).every((one) => one.entries >= one.expected) &&
|
|
60
|
+
progress.missing.length === 0
|
|
61
|
+
: attached;
|
|
62
|
+
const judgedDone = known && platform.judged?.ok === true;
|
|
63
|
+
const buildingDone = served || build?.status === 'ok';
|
|
64
|
+
const buildingCurrent = build !== null && (build.status === 'running' || build.status === 'queued' || build.status === 'failed');
|
|
65
|
+
|
|
66
|
+
const done = {
|
|
67
|
+
brief: briefDone,
|
|
68
|
+
laidOut: laidOutDone,
|
|
69
|
+
writing: writingDone,
|
|
70
|
+
judged: judgedDone,
|
|
71
|
+
attached,
|
|
72
|
+
building: buildingDone,
|
|
73
|
+
preview: served,
|
|
74
|
+
};
|
|
75
|
+
// In order: a later phase counts only once every phase before it does.
|
|
76
|
+
let before = true;
|
|
77
|
+
for (const key of PHASES) {
|
|
78
|
+
done[key] = done[key] && before;
|
|
79
|
+
before = done[key];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const current = buildingCurrent && !served ? 'building' : (PHASES.find((key) => !done[key]) ?? 'preview');
|
|
83
|
+
const index = PHASES.indexOf(current);
|
|
84
|
+
const phases = PHASES.map((key, i) => {
|
|
85
|
+
let state;
|
|
86
|
+
if (key === current) state = 'current';
|
|
87
|
+
else if (done[key]) state = 'done';
|
|
88
|
+
else if (!seen && !attached && i < 3) state = 'unseen';
|
|
89
|
+
else if (!known && i >= 3) state = 'unseen';
|
|
90
|
+
else if (i < index) state = 'unseen';
|
|
91
|
+
else state = 'left';
|
|
92
|
+
return { key, state };
|
|
93
|
+
});
|
|
94
|
+
return { phases, current, index, known, seen };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The sentence under the strip, for the current phase. */
|
|
98
|
+
export function sayPhase(where, progress, platform) {
|
|
99
|
+
const known = platform !== null && platform !== undefined && platform.error === undefined;
|
|
100
|
+
switch (where.current) {
|
|
101
|
+
case 'brief':
|
|
102
|
+
return 'Collecting the brief';
|
|
103
|
+
case 'laidOut':
|
|
104
|
+
return 'Laying the site out';
|
|
105
|
+
case 'writing': {
|
|
106
|
+
const pages = progress?.pages ?? [];
|
|
107
|
+
const written = pages.filter((one) => one.written).length;
|
|
108
|
+
return `Writing the pages — ${written} of ${pages.length} with their text · still to do: ${progress?.missing.length ?? 0}`;
|
|
109
|
+
}
|
|
110
|
+
case 'judged':
|
|
111
|
+
if (!known || !platform.judged) return 'To be checked before the push';
|
|
112
|
+
return platform.judged.ok ? 'Checked, clean' : `Checked — findings to fix: ${platform.judged.findings}`;
|
|
113
|
+
case 'attached':
|
|
114
|
+
return 'To be pushed and attached';
|
|
115
|
+
case 'building': {
|
|
116
|
+
const build = known ? platform.build : null;
|
|
117
|
+
if (!build) return 'To be built';
|
|
118
|
+
if (build.status === 'failed') return `The build failed — ${build.error || 'see its log in the panel'}`;
|
|
119
|
+
if (build.status === 'queued') return 'A build is queued';
|
|
120
|
+
return build.stage_of ? `Building — step ${build.stage_num} of ${build.stage_of}` : 'Building';
|
|
121
|
+
}
|
|
122
|
+
default:
|
|
123
|
+
return 'The preview is ready';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* What the developer can do in the panel meanwhile: the setting up's steps
|
|
129
|
+
* that are not the agent's — open first, then done.
|
|
130
|
+
*/
|
|
131
|
+
export function meanwhile(platform) {
|
|
132
|
+
const steps = platform?.steps ?? [];
|
|
133
|
+
const named = (step) => ({ key: step.key, name: STEP_NAMES[step.key] ?? step.key, done: step.done === true });
|
|
134
|
+
const theirs = steps.filter((step) => step.whose !== 'agent');
|
|
135
|
+
return {
|
|
136
|
+
open: theirs.filter((step) => !step.done).map(named),
|
|
137
|
+
done: theirs.filter((step) => step.done).map(named),
|
|
138
|
+
};
|
|
139
|
+
}
|