@cardinali-studio/adminkit 1.0.0

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 (181) hide show
  1. package/dist/api.d.ts +195 -0
  2. package/dist/api.js +118 -0
  3. package/dist/app/App.d.ts +3 -0
  4. package/dist/app/App.js +320 -0
  5. package/dist/app/app.css +2068 -0
  6. package/dist/app/index.d.ts +18 -0
  7. package/dist/app/index.js +33 -0
  8. package/dist/app/lib/bookings.d.ts +132 -0
  9. package/dist/app/lib/bookings.js +79 -0
  10. package/dist/app/lib/deliveries.d.ts +95 -0
  11. package/dist/app/lib/deliveries.js +104 -0
  12. package/dist/app/lib/delivery-drop.d.ts +1 -0
  13. package/dist/app/lib/delivery-drop.js +72 -0
  14. package/dist/app/lib/delivery-upload.d.ts +78 -0
  15. package/dist/app/lib/delivery-upload.js +585 -0
  16. package/dist/app/lib/groups.d.ts +9 -0
  17. package/dist/app/lib/groups.js +32 -0
  18. package/dist/app/lib/home.d.ts +11 -0
  19. package/dist/app/lib/home.js +67 -0
  20. package/dist/app/lib/overview.d.ts +82 -0
  21. package/dist/app/lib/overview.js +68 -0
  22. package/dist/app/lib/pages.d.ts +24 -0
  23. package/dist/app/lib/pages.js +156 -0
  24. package/dist/app/lib/publish.d.ts +18 -0
  25. package/dist/app/lib/publish.js +63 -0
  26. package/dist/app/lib/reorder.d.ts +3 -0
  27. package/dist/app/lib/reorder.js +31 -0
  28. package/dist/app/lib/requests.d.ts +10 -0
  29. package/dist/app/lib/requests.js +67 -0
  30. package/dist/app/lib/router.d.ts +1 -0
  31. package/dist/app/lib/router.js +14 -0
  32. package/dist/app/lib/settings.d.ts +29 -0
  33. package/dist/app/lib/settings.js +82 -0
  34. package/dist/app/lib/slug.d.ts +3 -0
  35. package/dist/app/lib/slug.js +23 -0
  36. package/dist/app/lib/text.d.ts +1 -0
  37. package/dist/app/lib/text.js +13 -0
  38. package/dist/app/lib/theme.d.ts +7 -0
  39. package/dist/app/lib/theme.js +43 -0
  40. package/dist/app/lib/upload-queue.d.ts +35 -0
  41. package/dist/app/lib/upload-queue.js +167 -0
  42. package/dist/app/routes/AboutEditor.d.ts +15 -0
  43. package/dist/app/routes/AboutEditor.js +122 -0
  44. package/dist/app/routes/Bookings.d.ts +21 -0
  45. package/dist/app/routes/Bookings.js +134 -0
  46. package/dist/app/routes/Deliveries.d.ts +37 -0
  47. package/dist/app/routes/Deliveries.js +563 -0
  48. package/dist/app/routes/GallerySectionsEditor.d.ts +12 -0
  49. package/dist/app/routes/GallerySectionsEditor.js +107 -0
  50. package/dist/app/routes/HomeEditor.d.ts +10 -0
  51. package/dist/app/routes/HomeEditor.js +188 -0
  52. package/dist/app/routes/Overview.d.ts +17 -0
  53. package/dist/app/routes/Overview.js +318 -0
  54. package/dist/app/routes/PageActions.d.ts +12 -0
  55. package/dist/app/routes/PageActions.js +93 -0
  56. package/dist/app/routes/PageEditor.d.ts +23 -0
  57. package/dist/app/routes/PageEditor.js +233 -0
  58. package/dist/app/routes/PageSeoEditor.d.ts +13 -0
  59. package/dist/app/routes/PageSeoEditor.js +102 -0
  60. package/dist/app/routes/PagesList.d.ts +10 -0
  61. package/dist/app/routes/PagesList.js +76 -0
  62. package/dist/app/routes/PortfolioWorksEditor.d.ts +12 -0
  63. package/dist/app/routes/PortfolioWorksEditor.js +139 -0
  64. package/dist/app/routes/Requests.d.ts +13 -0
  65. package/dist/app/routes/Requests.js +30 -0
  66. package/dist/app/routes/SiteSettingsEditor.d.ts +21 -0
  67. package/dist/app/routes/SiteSettingsEditor.js +66 -0
  68. package/dist/app/routes/UploadDock.d.ts +1 -0
  69. package/dist/app/routes/UploadDock.js +66 -0
  70. package/dist/app/routes/WorkshopEditionsEditor.d.ts +12 -0
  71. package/dist/app/routes/WorkshopEditionsEditor.js +101 -0
  72. package/dist/app/site.d.ts +37 -0
  73. package/dist/app/site.js +18 -0
  74. package/dist/app/ui/MediaSlot.d.ts +10 -0
  75. package/dist/app/ui/MediaSlot.js +20 -0
  76. package/dist/app/ui/OpenCard.d.ts +18 -0
  77. package/dist/app/ui/OpenCard.js +20 -0
  78. package/dist/app/ui/ParagraphEditor.d.ts +8 -0
  79. package/dist/app/ui/ParagraphEditor.js +37 -0
  80. package/dist/app/ui/dnd.d.ts +1 -0
  81. package/dist/app/ui/dnd.js +11 -0
  82. package/dist/app/ui/icons.d.ts +26 -0
  83. package/dist/app/ui/icons.js +45 -0
  84. package/dist/config.d.ts +5 -0
  85. package/dist/config.js +26 -0
  86. package/dist/form/MediaBrowser.d.ts +13 -0
  87. package/dist/form/MediaBrowser.js +24 -0
  88. package/dist/form/MediaPicker.d.ts +10 -0
  89. package/dist/form/MediaPicker.js +20 -0
  90. package/dist/form/SchemaForm.d.ts +21 -0
  91. package/dist/form/SchemaForm.js +67 -0
  92. package/dist/form/Trash.d.ts +1 -0
  93. package/dist/form/Trash.js +42 -0
  94. package/dist/form/fields/index.d.ts +1 -0
  95. package/dist/form/fields/index.js +110 -0
  96. package/dist/form/fields/objectList.d.ts +1 -0
  97. package/dist/form/fields/objectList.js +99 -0
  98. package/dist/form/index.d.ts +8 -0
  99. package/dist/form/index.js +8 -0
  100. package/dist/form/media/Lightbox.d.ts +4 -0
  101. package/dist/form/media/Lightbox.js +70 -0
  102. package/dist/form/media/MediaFolders.d.ts +20 -0
  103. package/dist/form/media/MediaFolders.js +116 -0
  104. package/dist/form/media/MediaGrid.d.ts +8 -0
  105. package/dist/form/media/MediaGrid.js +139 -0
  106. package/dist/form/media/UploadDialog.d.ts +4 -0
  107. package/dist/form/media/UploadDialog.js +37 -0
  108. package/dist/form/media/useMediaLibrary.d.ts +55 -0
  109. package/dist/form/media/useMediaLibrary.js +495 -0
  110. package/dist/form/prose/ProseBar.d.ts +21 -0
  111. package/dist/form/prose/ProseBar.js +111 -0
  112. package/dist/form/prose/ProseField.d.ts +11 -0
  113. package/dist/form/prose/ProseField.js +152 -0
  114. package/dist/form/prose/pages.d.ts +6 -0
  115. package/dist/form/prose/pages.js +13 -0
  116. package/dist/form/registry.d.ts +20 -0
  117. package/dist/form/registry.js +15 -0
  118. package/dist/frontmatter.d.ts +20 -0
  119. package/dist/frontmatter.js +202 -0
  120. package/dist/functions/_middleware.js +101 -0
  121. package/dist/functions/_shared/analytics-ga4.js +248 -0
  122. package/dist/functions/_shared/analytics-umami.js +119 -0
  123. package/dist/functions/_shared/assets.js +90 -0
  124. package/dist/functions/_shared/b2.js +300 -0
  125. package/dist/functions/_shared/delivery-crypto.js +99 -0
  126. package/dist/functions/_shared/delivery-purge.js +222 -0
  127. package/dist/functions/_shared/edge-cache.js +37 -0
  128. package/dist/functions/_shared/exif.js +53 -0
  129. package/dist/functions/_shared/github.js +138 -0
  130. package/dist/functions/_shared/imagesize.js +101 -0
  131. package/dist/functions/_shared/media-content.js +200 -0
  132. package/dist/functions/_shared/pending-media.js +98 -0
  133. package/dist/functions/_shared/refs.js +155 -0
  134. package/dist/functions/api/analytics.js +106 -0
  135. package/dist/functions/api/assets/delete.js +241 -0
  136. package/dist/functions/api/assets/folder.js +43 -0
  137. package/dist/functions/api/assets/folders.js +32 -0
  138. package/dist/functions/api/assets/move.js +253 -0
  139. package/dist/functions/api/assets/preview.js +17 -0
  140. package/dist/functions/api/assets/trash/delete.js +13 -0
  141. package/dist/functions/api/assets/trash/empty.js +16 -0
  142. package/dist/functions/api/assets/trash/restore.js +43 -0
  143. package/dist/functions/api/assets/trash.js +28 -0
  144. package/dist/functions/api/assets/upload.js +54 -0
  145. package/dist/functions/api/assets/usage.js +41 -0
  146. package/dist/functions/api/assets.js +94 -0
  147. package/dist/functions/api/deliveries.js +629 -0
  148. package/dist/functions/api/delivery/hero.js +21 -0
  149. package/dist/functions/api/delivery/image.js +35 -0
  150. package/dist/functions/api/delivery/multipart.js +261 -0
  151. package/dist/functions/api/pages/get.js +26 -0
  152. package/dist/functions/api/pages.js +354 -0
  153. package/dist/functions/api/publish.js +134 -0
  154. package/dist/functions/api/requests.js +57 -0
  155. package/dist/functions/api/settings.js +236 -0
  156. package/dist/functions/api/singleton.js +105 -0
  157. package/dist/functions/api/storage.js +125 -0
  158. package/dist/functions/index.js +124 -0
  159. package/dist/functions/site.js +38 -0
  160. package/dist/gallery.d.ts +28 -0
  161. package/dist/gallery.js +18 -0
  162. package/dist/index.d.ts +10 -0
  163. package/dist/index.js +10 -0
  164. package/dist/model.d.ts +2 -0
  165. package/dist/model.js +47 -0
  166. package/dist/normalize.d.ts +50 -0
  167. package/dist/normalize.js +120 -0
  168. package/dist/preview.d.ts +3 -0
  169. package/dist/preview.js +82 -0
  170. package/dist/prose.d.ts +17 -0
  171. package/dist/prose.js +205 -0
  172. package/dist/registries.d.ts +43 -0
  173. package/dist/registries.js +29 -0
  174. package/dist/serialize.d.ts +26 -0
  175. package/dist/serialize.js +136 -0
  176. package/dist/testing.d.ts +26 -0
  177. package/dist/testing.js +66 -0
  178. package/dist/theme.css +636 -0
  179. package/dist/yaml.d.ts +8 -0
  180. package/dist/yaml.js +90 -0
  181. package/package.json +97 -0
@@ -0,0 +1,82 @@
1
+ /* Stato delle impostazioni globali.
2
+ *
3
+ * È un singleton come la home, ma usa JSON perché lo leggono sia Hugo sia il middleware che
4
+ * protegge il sito. La password digitata resta soltanto nello stato del form: il backend
5
+ * restituisce sempre il campo vuoto e conserva esclusivamente la verifica crittografica.
6
+ */
7
+ import { useCallback, useEffect, useState } from "react";
8
+ import { ApiError } from "../../index.js";
9
+ const empty = {
10
+ siteTitle: "",
11
+ shortDescription: "",
12
+ favicon: "",
13
+ ownerName: "",
14
+ siteDescription: "",
15
+ seoWebsiteTitle: "",
16
+ searchEngineIndexing: true,
17
+ blockAiTraining: false,
18
+ analyticsTag: "",
19
+ features: { analytics: false, appointments: false },
20
+ passwordEnabled: false,
21
+ password: "",
22
+ };
23
+ async function call(url, init) {
24
+ const res = await fetch(url, { cache: "no-store", ...init });
25
+ const body = await res.json().catch(() => null);
26
+ if (!res.ok || body?.ok === false) {
27
+ throw new ApiError(body?.error ?? `HTTP ${res.status}`, body?.code ?? (res.status === 401 ? "unauthenticated" : "error"), res.status);
28
+ }
29
+ return body;
30
+ }
31
+ export function useSiteSettings(active) {
32
+ const [data, setData] = useState(null);
33
+ const [error, setError] = useState(null);
34
+ const [dirty, setDirty] = useState(false);
35
+ const [saving, setSaving] = useState(false);
36
+ const [notice, setNotice] = useState(null);
37
+ const load = useCallback(async () => {
38
+ try {
39
+ const result = await call("/admin/api/settings");
40
+ setData({ ...empty, ...result.settings, password: "" });
41
+ setError(null);
42
+ setDirty(false);
43
+ }
44
+ catch (e) {
45
+ setError(e.message);
46
+ }
47
+ }, []);
48
+ useEffect(() => {
49
+ if (active && data === null && error === null)
50
+ void load();
51
+ }, [active, data, error, load]);
52
+ const save = useCallback(async () => {
53
+ if (!data)
54
+ return;
55
+ setSaving(true);
56
+ setNotice(null);
57
+ try {
58
+ const result = await call("/admin/api/settings", {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify(data),
62
+ });
63
+ setData({ ...empty, ...result.settings, password: "" });
64
+ setDirty(false);
65
+ setNotice("Pubblicato — build in corso");
66
+ }
67
+ catch (e) {
68
+ const err = e;
69
+ setNotice(err.code === "unauthenticated"
70
+ ? "Sessione scaduta: ricarica la pagina per rientrare."
71
+ : err.message);
72
+ }
73
+ finally {
74
+ setSaving(false);
75
+ }
76
+ }, [data]);
77
+ const change = useCallback((next) => {
78
+ setData(next);
79
+ setDirty(true);
80
+ }, []);
81
+ return { data, error, dirty, saving, notice, load, change, save, setNotice };
82
+ }
@@ -0,0 +1,3 @@
1
+ export declare function slugify(s: string): string;
2
+ /** Slug libero, aggiungendo -2, -3… finché non collide più. */
3
+ export declare function uniqueSlug(base: string, taken: readonly string[]): string;
@@ -0,0 +1,23 @@
1
+ /* Slug: è il nome della cartella in `content/`, quindi anche l'URL della pagina.
2
+ * Il server accetta solo `^[a-z0-9-]+$` (pages.js), e qui si produce già quella forma invece
3
+ * di far arrivare al salvataggio un valore che verrà rifiutato. */
4
+ export function slugify(s) {
5
+ return String(s)
6
+ .normalize("NFKD").replace(/[\u0300-\u036f]/g, "") // via gli accenti
7
+ .toLowerCase()
8
+ .replace(/[^a-z0-9]+/g, "-")
9
+ .replace(/^-+|-+$/g, "")
10
+ .slice(0, 60) || "pagina";
11
+ }
12
+ /** Slug libero, aggiungendo -2, -3… finché non collide più. */
13
+ export function uniqueSlug(base, taken) {
14
+ const b = slugify(base);
15
+ if (!taken.includes(b))
16
+ return b;
17
+ for (let i = 2; i < 500; i++) {
18
+ const s = `${b}-${i}`;
19
+ if (!taken.includes(s))
20
+ return s;
21
+ }
22
+ return `${b}-${Date.now().toString(36)}`;
23
+ }
@@ -0,0 +1 @@
1
+ export declare function proseStats(text: string): string;
@@ -0,0 +1,13 @@
1
+ /* Quanto testo c'è, detto come lo direbbe una persona.
2
+ *
3
+ * Non è una statistica: è il modo di accorgersi di aver cancellato un paragrafo senza rileggere
4
+ * tutto. Sta qui perché la usano la biografia della pagina "chi sono" e l'introduzione dei
5
+ * workshop — gli unici due posti in cui il corpo markdown è il contenuto principale.
6
+ */
7
+ export function proseStats(text) {
8
+ const paragraphs = text.split(/\n\s*\n/).filter((piece) => piece.trim()).length;
9
+ const words = text.trim() ? text.trim().split(/\s+/).length : 0;
10
+ if (!words)
11
+ return "Vuoto";
12
+ return `${paragraphs} ${paragraphs === 1 ? "paragrafo" : "paragrafi"} · ${words} parole`;
13
+ }
@@ -0,0 +1,7 @@
1
+ export type Theme = "light" | "dark";
2
+ export declare function initialTheme(): Theme;
3
+ export declare function applyTheme(theme: Theme): void;
4
+ export declare function useTheme(): {
5
+ theme: Theme;
6
+ toggle: () => void;
7
+ };
@@ -0,0 +1,43 @@
1
+ /* Il tema chiaro/scuro del pannello: dove vive, come si applica, come si ricorda.
2
+ *
3
+ * L'attributo si scrive su `document.documentElement`, non su `.app`: `body` (in app.css) ha
4
+ * `background: var(--ak-bg)` ed è un ANTENATO di `.app`, non un discendente — se l'attributo
5
+ * stesse su `.app`, il fondo di `body` resterebbe quello chiaro e si vedrebbe un lampo chiaro
6
+ * nell'overscroll di Safari. Sulla radice lo eredita tutto, `body` compreso.
7
+ */
8
+ import { useEffect, useState } from "react";
9
+ import { storageKey } from "../../index.js";
10
+ const KEY = storageKey("theme"); // col prefisso del sito davanti, es. «<prefisso>_theme»
11
+ function systemPrefersDark() {
12
+ return typeof matchMedia !== "undefined" && matchMedia("(prefers-color-scheme: dark)").matches;
13
+ }
14
+ /* Pura e idempotente: la richiama sia il bootstrap sincrono in main.tsx (per evitare il lampo
15
+ del tema sbagliato al primo render), sia questo modulo, per inizializzare lo stato React allo
16
+ stesso valore già scritto sul DOM. */
17
+ export function initialTheme() {
18
+ try {
19
+ const saved = localStorage.getItem(KEY);
20
+ if (saved === "light" || saved === "dark")
21
+ return saved;
22
+ }
23
+ catch {
24
+ /* localStorage non disponibile: si ripiega sulla preferenza di sistema. */
25
+ }
26
+ return systemPrefersDark() ? "dark" : "light";
27
+ }
28
+ export function applyTheme(theme) {
29
+ document.documentElement.setAttribute("data-theme", theme);
30
+ }
31
+ export function useTheme() {
32
+ const [theme, setTheme] = useState(initialTheme);
33
+ useEffect(() => {
34
+ applyTheme(theme);
35
+ try {
36
+ localStorage.setItem(KEY, theme);
37
+ }
38
+ catch {
39
+ /* Niente da fare: la scelta vale solo per questa sessione. */
40
+ }
41
+ }, [theme]);
42
+ return { theme, toggle: () => setTheme((t) => (t === "light" ? "dark" : "light")) };
43
+ }
@@ -0,0 +1,35 @@
1
+ import { type DeliveryUploadProgress } from "./delivery-upload";
2
+ export type UploadState = "running" | "paused" | "done" | "failed";
3
+ export interface UploadJob {
4
+ /** L'id del pacchetto: uno per caricamento. */
5
+ id: string;
6
+ deliveryId: string;
7
+ title: string;
8
+ totalFiles: number;
9
+ sourceBytes: number;
10
+ state: UploadState;
11
+ progress: DeliveryUploadProgress | null;
12
+ error?: string;
13
+ skipped?: number;
14
+ }
15
+ interface StartInput {
16
+ deliveryId: string;
17
+ packageId: string;
18
+ title: string;
19
+ files: File[];
20
+ options: {
21
+ galleryEnabled: boolean;
22
+ webEnabled: boolean;
23
+ };
24
+ }
25
+ export declare function useUploads(): UploadJob[];
26
+ export declare function useUploadCompletions(): number;
27
+ export declare function uploadFor(deliveryId: string, list: UploadJob[]): UploadJob | undefined;
28
+ export declare function isBusy(job: UploadJob | undefined): boolean;
29
+ export declare function startUpload(input: StartInput): void;
30
+ export declare function pauseUpload(id: string): void;
31
+ export declare function resumeUpload(id: string): void;
32
+ export declare function cancelUpload(id: string): void;
33
+ export declare function dismissUpload(id: string): void;
34
+ export declare function hasRunningUploads(): boolean;
35
+ export {};
@@ -0,0 +1,167 @@
1
+ /* La coda dei caricamenti — fuori da React, di proposito.
2
+ *
3
+ * Prima lo stato dell'upload viveva DENTRO la schermata del transfer: uscire da lì lo
4
+ * uccideva, e per questo il pannello si disabilitava da solo mentre caricava. Su quaranta
5
+ * giga significa ore di pannello inutilizzabile per qualsiasi altra cosa.
6
+ *
7
+ * Qui lo stato sta a livello di modulo — non in un contesto, non in un componente — quindi
8
+ * non c'è niente che possa smontarlo. I componenti si limitano a guardarlo con
9
+ * `useSyncExternalStore`. È anche il motivo per cui questo file non ha JSX: la coda non è
10
+ * un pezzo di interfaccia, l'interfaccia è `routes/UploadDock.tsx`.
11
+ *
12
+ * Chi lo usa:
13
+ * Deliveries.tsx avvia (`startUpload`) e mostra l'avanzamento del proprio transfer
14
+ * UploadDock.tsx mostra tutti i caricamenti, e li mette in pausa o li annulla
15
+ * App.tsx ricarica l'elenco quando uno finisce (`useUploadCompletions`)
16
+ */
17
+ import { useSyncExternalStore } from "react";
18
+ import { UploadCancelled, uploadDeliveryPackage, } from "./delivery-upload";
19
+ import { deliveryAction } from "./deliveries";
20
+ /* ---- lo stato ------------------------------------------------------------------------ */
21
+ let jobs = [];
22
+ let completions = 0;
23
+ const gates = new Map();
24
+ const listeners = new Set();
25
+ function emit() {
26
+ jobs = [...jobs];
27
+ for (const listener of listeners)
28
+ listener();
29
+ }
30
+ function patch(id, change) {
31
+ jobs = jobs.map((job) => (job.id === id ? { ...job, ...change } : job));
32
+ for (const listener of listeners)
33
+ listener();
34
+ }
35
+ function subscribe(listener) {
36
+ listeners.add(listener);
37
+ return () => { listeners.delete(listener); };
38
+ }
39
+ /* `jobs` cambia identità solo dentro emit/patch: `useSyncExternalStore` può confrontarla. */
40
+ export function useUploads() {
41
+ return useSyncExternalStore(subscribe, () => jobs, () => jobs);
42
+ }
43
+ /* Un numero che cresce quando un caricamento FINISCE, comunque sia andato. Chi deve solo
44
+ rileggere i dati si abbona a questo e non all'avanzamento, altrimenti si ridisegnerebbe
45
+ due volte al secondo per tutta la durata dell'upload. */
46
+ export function useUploadCompletions() {
47
+ return useSyncExternalStore(subscribe, () => completions, () => completions);
48
+ }
49
+ export function uploadFor(deliveryId, list) {
50
+ return list.find((job) => job.deliveryId === deliveryId);
51
+ }
52
+ export function isBusy(job) {
53
+ return !!job && (job.state === "running" || job.state === "paused");
54
+ }
55
+ /* ---- il cancello --------------------------------------------------------------------- */
56
+ /* Pausa e annullamento passano da qui: il motore (delivery-upload.ts) chiama `gate()` a ogni
57
+ file e a ogni parte, e questa promessa lo tiene fermo finché non si riprende. */
58
+ class Gate {
59
+ paused = false;
60
+ cancelled = false;
61
+ waiting = [];
62
+ async gate() {
63
+ if (this.cancelled)
64
+ throw new UploadCancelled();
65
+ while (this.paused) {
66
+ await new Promise((resolve) => this.waiting.push(resolve));
67
+ if (this.cancelled)
68
+ throw new UploadCancelled();
69
+ }
70
+ }
71
+ wake() {
72
+ const waiting = this.waiting;
73
+ this.waiting = [];
74
+ for (const resolve of waiting)
75
+ resolve();
76
+ }
77
+ }
78
+ /* ---- i comandi ------------------------------------------------------------------------ */
79
+ export function startUpload(input) {
80
+ const gate = new Gate();
81
+ gates.set(input.packageId, gate);
82
+ jobs = [...jobs.filter((job) => job.id !== input.packageId), {
83
+ id: input.packageId,
84
+ deliveryId: input.deliveryId,
85
+ title: input.title,
86
+ totalFiles: input.files.length,
87
+ sourceBytes: input.files.reduce((sum, file) => sum + file.size, 0),
88
+ state: "running",
89
+ progress: null,
90
+ }];
91
+ emit();
92
+ void run(input, gate);
93
+ }
94
+ async function run(input, gate) {
95
+ const id = input.packageId;
96
+ try {
97
+ const result = await uploadDeliveryPackage(input.deliveryId, id, input.files, input.options, (progress) => patch(id, { progress }), gate);
98
+ /* Chiusura e attivazione stanno QUI e non nella schermata: se finiscono mentre il
99
+ fotografo sta guardando un'altra sezione, devono succedere lo stesso. */
100
+ await deliveryAction({
101
+ action: "finalizePackage", id: input.deliveryId, packageId: id,
102
+ manifest: result.manifest, fileCount: result.fileCount,
103
+ sourceBytes: result.sourceBytes, storedBytes: result.storedBytes,
104
+ });
105
+ await deliveryAction({ action: "activate", id: input.deliveryId }).catch(() => { });
106
+ patch(id, { state: "done", progress: null, skipped: result.manifest.skipped.length });
107
+ }
108
+ catch (error) {
109
+ if (error instanceof UploadCancelled) {
110
+ /* Annullare vuol dire «come se non l'avessi mai iniziato»: il pacchetto sparisce, e
111
+ con lui il multipart aperto su B2. */
112
+ await deliveryAction({
113
+ action: "deletePackage", id: input.deliveryId, packageId: id,
114
+ }).catch(() => { });
115
+ jobs = jobs.filter((job) => job.id !== id);
116
+ }
117
+ else {
118
+ patch(id, { state: "failed", progress: null, error: readable(error) });
119
+ }
120
+ }
121
+ finally {
122
+ gates.delete(id);
123
+ completions += 1;
124
+ emit();
125
+ }
126
+ }
127
+ export function pauseUpload(id) {
128
+ const gate = gates.get(id);
129
+ if (!gate)
130
+ return;
131
+ gate.paused = true;
132
+ patch(id, { state: "paused" });
133
+ }
134
+ export function resumeUpload(id) {
135
+ const gate = gates.get(id);
136
+ if (!gate)
137
+ return;
138
+ gate.paused = false;
139
+ gate.wake();
140
+ patch(id, { state: "running" });
141
+ }
142
+ export function cancelUpload(id) {
143
+ const gate = gates.get(id);
144
+ if (!gate)
145
+ return;
146
+ gate.cancelled = true;
147
+ gate.paused = false;
148
+ gate.wake();
149
+ }
150
+ /* Toglie dalla vista un caricamento già finito. Su uno in corso non fa niente: per quello
151
+ c'è `cancelUpload`, che è un'altra cosa e va detta con un'altra parola. */
152
+ export function dismissUpload(id) {
153
+ jobs = jobs.filter((job) => job.id === id ? isBusy(job) : true);
154
+ emit();
155
+ }
156
+ export function hasRunningUploads() {
157
+ return jobs.some((job) => job.state === "running");
158
+ }
159
+ function readable(error) {
160
+ const raw = error instanceof Error ? error.message : String(error);
161
+ if ((error instanceof DOMException && error.name === "NotReadableError") ||
162
+ raw.includes("requested file could not be read")) {
163
+ return "Il browser non riesce più a leggere uno dei file scelti. " +
164
+ "Riseleziona la stessa cartella e premi «Riprendi».";
165
+ }
166
+ return raw;
167
+ }
@@ -0,0 +1,15 @@
1
+ import type { AssetFile } from "../../index.js";
2
+ import { normalizeCredits, type Credit } from "../../normalize.js";
3
+ export { normalizeCredits };
4
+ import type { PageModel } from "../lib/pages";
5
+ export type { Credit };
6
+ type Picker = (o: {
7
+ multiple: boolean;
8
+ }) => Promise<AssetFile[] | null>;
9
+ export declare function AboutEditor({ page, onChange, bodyState, pickMediaFiles }: {
10
+ page: PageModel;
11
+ onChange: (next: PageModel) => void;
12
+ /** Il corpo markdown si rilegge dal repo all'apertura: qui si dice solo a che punto è. */
13
+ bodyState: "idle" | "loading" | "error";
14
+ pickMediaFiles?: Picker;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /* La pagina "chi sono".
3
+ *
4
+ * ── L'idea ─────────────────────────────────────────────────────────────────────────────────
5
+ * Come per gallery e portfolio: l'editor mostra LA PAGINA, non i campi della pagina. Questa ha
6
+ * un'anatomia fissa e riconoscibile — biografia a sinistra, ritratto fermo a destra,
7
+ * riconoscimenti in coda (ABOUT.md) — e la schermata la rispecchia, ritratto `sticky` compreso.
8
+ *
9
+ * Prima cadeva nel ramo generico di `PageEditor`, cioè `SchemaForm` su tutto lo schema `Page`:
10
+ * comparivano «Lavori», «Edizioni» e «Sezioni» — campi di portfolio, workshop e gallery, che il
11
+ * form disegna perché lo schema li dichiara e lui non sa che tipo di pagina sta modificando — e
12
+ * la biografia, che è il contenuto principale, finiva ultima in una textarea grigia.
13
+ *
14
+ * NON è modulare come le gallery, e non è una dimenticanza: là l'ordine e la composizione dei
15
+ * blocchi SONO il contenuto, qui la struttura è una sola e la disegna `about.html`. Un «aggiungi
16
+ * sezione» prometterebbe una libertà che il template non sa onorare.
17
+ *
18
+ * ── La biografia resta markdown ────────────────────────────────────────────────────────────
19
+ * Il corpo è una stringa e tale rimane: `ParagraphEditor` lavora su `string[]`, e spezzare il
20
+ * corpo in paragrafi per poi ricomporlo con `\n\n` riscriverebbe il testo di chi usa elenchi,
21
+ * titoli o righe di codice — cose che nel corpo markdown sono legittime (nel front matter no,
22
+ * ed è per quello che là i paragrafi sono un array). Cambia solo come lo si guarda: colonna a
23
+ * misura di lettura, altezza che segue il testo, e il conteggio sotto.
24
+ *
25
+ * ── I riconoscimenti ───────────────────────────────────────────────────────────────────────
26
+ * Tre campi in riga, nella grafia con cui escono sul sito: il ruolo è testo corrente, il nome è
27
+ * l'entità in evidenza (maiuscoletto, colore d'accento — vedi `.ab-credit-name` in about.css) e
28
+ * l'indirizzo è il campo quieto sotto. L'indirizzo si vede SEMPRE invece di aprirsi a richiesta:
29
+ * "aperto/chiuso" sarebbe uno stato per riga da ricalcolare a ogni riordino ed eliminazione —
30
+ * il prezzo già pagato in `PortfolioWorksEditor` con `openAt` — per nascondere un campo che è
31
+ * alto una riga.
32
+ *
33
+ * Le chiavi del drag & drop sono POSIZIONALI (`credito-2`), come per i lavori: i riconoscimenti
34
+ * non hanno un `id` nel front matter e non glielo si aggiunge solo per far funzionare
35
+ * l'interfaccia. Qui non costa niente, perché non c'è nessuno stato "quale riga è aperta".
36
+ *
37
+ * ── I recapiti ─────────────────────────────────────────────────────────────────────────────
38
+ * Non sono campi di questa pagina (stanno nel blocco `contact` della home, vedi ABOUT.md) — ma
39
+ * nell'admin un'assenza si legge come una dimenticanza. Quindi si dicono: in sola lettura, con
40
+ * il link a dove si cambiano e l'avviso che cambiarli cambia anche la home. È lo stesso recapito.
41
+ */
42
+ import { useEffect, useState } from "react";
43
+ import { DndContext, closestCenter } from "@dnd-kit/core";
44
+ import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
45
+ import { CSS } from "@dnd-kit/utilities";
46
+ import { ProseField } from "../../form/index.js";
47
+ import { normalizeCredits } from "../../normalize.js";
48
+ export { normalizeCredits };
49
+ import { groupSlug } from "../lib/groups";
50
+ import { proseStats } from "../lib/text";
51
+ import { MediaSlot } from "../ui/MediaSlot";
52
+ import { useDragSensors } from "../ui/dnd";
53
+ import { Icon } from "../ui/icons";
54
+ const str = (value) => (typeof value === "string" ? value : "");
55
+ const list = (value) => (Array.isArray(value) ? value : []);
56
+ /* ---- La biografia -------------------------------------------------------------------------- */
57
+ /* Il campo cresce col testo da sé — è un contenteditable, non una textarea — e questo toglie il
58
+ caso in cui una biografia di tre paragrafi scorre dentro una finestrella mentre la pagina
59
+ intorno ha spazio libero. La barra di formattazione arriva con `ProseField`, come ovunque. */
60
+ function ProseEditor({ value, onChange }) {
61
+ return (_jsx(ProseField, { className: "pw-field ed-prose", value: value, placeholder: "Racconta chi sei.\nVai a capo per iniziare un paragrafo nuovo.", onChange: onChange }));
62
+ }
63
+ /* ---- Un riconoscimento ---------------------------------------------------------------------- */
64
+ function SortableCredit({ id, credit, index, total, onPatch, onRemove }) {
65
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
66
+ const linked = !!credit.href.trim();
67
+ return (_jsxs("li", { ref: setNodeRef, className: "abo-credit" + (isDragging ? " is-dragging" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("div", { className: "abo-credit-main", children: [_jsx("button", { type: "button", className: "pw-grip", "aria-label": `Riordina ${credit.name || "riconoscimento senza nome"}, posizione ${index + 1} di ${total}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsx("input", { className: "pw-field abo-role", value: credit.label, maxLength: 60, placeholder: "Member of", "aria-label": "Ruolo", onChange: (event) => onPatch({ label: event.target.value }) }), _jsx("input", { className: "pw-field abo-name", value: credit.name, maxLength: 120, placeholder: "Peli Pro Team", "aria-label": "Nome", onChange: (event) => onPatch({ name: event.target.value }) }), _jsx("button", { type: "button", className: "pw-icon-btn is-danger", "aria-label": "Elimina riconoscimento", onClick: onRemove, children: _jsx(Icon.trash, {}) })] }), _jsxs("div", { className: "abo-credit-link" + (linked ? " is-linked" : ""), children: [_jsx("span", { className: "abo-link-mark", "aria-hidden": "true", children: _jsx(Icon.out, {}) }), _jsx("input", { className: "pw-field abo-href", value: credit.href, maxLength: 300, placeholder: "https://\u2026 \u2014 facoltativo: senza, il nome resta testo", "aria-label": "Indirizzo", onChange: (event) => onPatch({ href: event.target.value }) })] })] }));
68
+ }
69
+ function CreditsEditor({ items, onChange }) {
70
+ const sensors = useDragSensors();
71
+ const ids = items.map((_, index) => `credito-${index}`);
72
+ const finish = ({ active, over }) => {
73
+ if (!over || active.id === over.id)
74
+ return;
75
+ const from = ids.indexOf(String(active.id));
76
+ const to = ids.indexOf(String(over.id));
77
+ if (from >= 0 && to >= 0)
78
+ onChange(arrayMove(items, from, to));
79
+ };
80
+ return (_jsxs("section", { className: "abo-block", "aria-label": "Riconoscimenti e collaborazioni", children: [_jsxs("header", { className: "pw-head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Riconoscimenti" }), _jsx("p", { children: "Ruolo e nome, nell\u2019ordine in cui si leggono sotto la biografia." })] }), _jsxs("button", { type: "button", className: "btn-soft is-primary", onClick: () => onChange([...items, { label: "", name: "", href: "" }]), children: [_jsx(Icon.plus, {}), " Aggiungi voce"] })] }), items.length === 0 ? (_jsxs("p", { className: "pw-empty-note", children: ["\u00ABMember of ", _jsx("strong", { children: "Peli Pro Team" }), "\u00BB, \u00ABContributor for ", _jsx("strong", { children: "Food & Wine Italy" }), "\u00BB\u2026 Compaiono in coda alla biografia."] })) : (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: finish, children: _jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: _jsx("ul", { className: "abo-credits", children: items.map((credit, index) => (_jsx(SortableCredit, { id: ids[index], credit: credit, index: index, total: items.length, onPatch: (patch) => onChange(items.map((item, i) => (i === index ? { ...item, ...patch } : item))), onRemove: () => onChange(items.filter((_, i) => i !== index)) }, ids[index]))) }) }) }))] }));
81
+ }
82
+ /* Sola lettura, e tollerante: se la chiamata non riesce — in anteprima locale senza
83
+ GITHUB_TOKEN è un 500 — il riquadro resta con il suo testo e il suo link, che è la parte che
84
+ serve davvero. Un errore rosso qui parlerebbe di una cosa che non si stava facendo. */
85
+ function useSiteContact() {
86
+ const [contact, setContact] = useState(null);
87
+ useEffect(() => {
88
+ let alive = true;
89
+ fetch("/admin/api/singleton?name=home", { cache: "no-store" })
90
+ .then((res) => (res.ok ? res.json() : null))
91
+ .then((body) => {
92
+ const found = body?.data?.contact;
93
+ if (alive && found && typeof found === "object")
94
+ setContact(found);
95
+ })
96
+ .catch(() => { });
97
+ return () => { alive = false; };
98
+ }, []);
99
+ return contact;
100
+ }
101
+ function ContactsNote() {
102
+ const contact = useSiteContact();
103
+ const rows = [
104
+ ["Email", str(contact?.email)],
105
+ ["Telefono", str(contact?.phone)],
106
+ ["Dove", str(contact?.city)],
107
+ ["Instagram", str(contact?.instagram)],
108
+ ].filter(([, value]) => value);
109
+ return (_jsxs("section", { className: "abo-note", children: [_jsxs("div", { className: "abo-note-text", children: [_jsx("h4", { children: "I recapiti non sono campi di questa pagina" }), _jsx("p", { children: "Sono quelli del sito: la pagina li legge dal blocco \u00ABScrivimi\u00BB della home, che alimenta anche la schermata dei contatti. Cambiarli l\u00EC li cambia in tutti e due i posti \u2014 \u00E8 lo stesso recapito." })] }), !!rows.length && (_jsx("dl", { className: "abo-note-list", children: rows.map(([label, value]) => (_jsxs("div", { children: [_jsx("dt", { children: label }), _jsx("dd", { children: value })] }, label))) })), _jsx("a", { className: "btn-soft", href: `#/home/${groupSlug("Contatto")}`, children: "Modifica nella home" })] }));
110
+ }
111
+ /* ---- La schermata ---------------------------------------------------------------------------- */
112
+ export function AboutEditor({ page, onChange, bodyState, pickMediaFiles }) {
113
+ const bio = str(page.body);
114
+ const portrait = str(page.portrait);
115
+ const credits = normalizeCredits(page.credits);
116
+ const choosePortrait = async () => {
117
+ const file = (await pickMediaFiles?.({ multiple: false }))?.[0];
118
+ if (file)
119
+ onChange({ ...page, portrait: file.url });
120
+ };
121
+ return (_jsxs("section", { className: "abo", "aria-label": "Pagina chi sono", children: [_jsxs("div", { className: "abo-main", children: [_jsxs("section", { className: "abo-block", "aria-label": "Biografia", children: [_jsxs("header", { className: "pw-head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Biografia" }), _jsx("p", { children: "Il testo che si legge accanto al ritratto." })] }), _jsx("span", { className: "ed-count", children: bodyState === "loading" ? "Carico…" : proseStats(bio) })] }), bodyState === "error" && (_jsx("p", { className: "ak-error", children: "Non sono riuscito a caricare il testo dal sito. Ricarica prima di modificarlo." })), _jsx(ProseEditor, { value: bio, onChange: (body) => onChange({ ...page, body }) }), _jsx("p", { className: "pw-empty-note", children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] }), _jsx(CreditsEditor, { items: credits, onChange: (next) => onChange({ ...page, credits: next }) }), _jsx(ContactsNote, {})] }), _jsx("aside", { className: "abo-side", children: _jsxs("div", { className: "abo-side-inner", children: [_jsx("h3", { children: "Ritratto" }), _jsx(MediaSlot, { value: portrait, ratio: "portrait", empty: "Scegli il ritratto", onPick: () => void choosePortrait(), onClear: () => onChange({ ...page, portrait: "" }) }), _jsx("p", { className: "pw-empty-note", children: "Resta fermo accanto al testo mentre si scorre. Su schermo stretto passa sopra la biografia: incolonnati, \u00E8 la foto ad aprire la pagina." })] }) })] }));
122
+ }
@@ -0,0 +1,21 @@
1
+ import type { Booking, useBookings } from "../lib/bookings";
2
+ type Store = ReturnType<typeof useBookings>;
3
+ export declare function BookingsList({ rows, current, onOpen, onAgenda, onSettings, }: {
4
+ rows: Booking[];
5
+ current?: string;
6
+ onOpen: (id: string) => void;
7
+ onAgenda: () => void;
8
+ onSettings: () => void;
9
+ }): import("react").JSX.Element;
10
+ export declare function BookingAgenda({ store, onOpen }: {
11
+ store: Store;
12
+ onOpen: (id: string) => void;
13
+ }): import("react").JSX.Element;
14
+ export declare function BookingDetail({ row, store }: {
15
+ row: Booking;
16
+ store: Store;
17
+ }): import("react").JSX.Element;
18
+ export declare function BookingSettingsHub({ store }: {
19
+ store: Store;
20
+ }): import("react").JSX.Element;
21
+ export {};