@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,563 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /* Consegne — l'interfaccia.
3
+ *
4
+ * UNA schermata sola per un transfer, che sia nuovo o già spedito: prima c'erano due form
5
+ * diversi per lo stesso oggetto, con etichette diverse per lo stesso campo e — il peggio —
6
+ * default opposti sugli stessi due interruttori. Qui il transfer nuovo è semplicemente questa
7
+ * schermata con i campi vuoti.
8
+ *
9
+ * La forma viene da WeTransfer, allargata allo spazio che abbiamo: a sinistra la colonna del
10
+ * lavoro (file, nome, messaggio, link), a destra le opzioni raggruppate. Sotto, a piena
11
+ * larghezza, la copertina — che ha bisogno di una griglia di foto e in una colonna stretta non
12
+ * ci sta.
13
+ *
14
+ * Due parole che NON esistono più, e perché:
15
+ * «attivazione» non corrispondeva a niente che l'utente volesse fare. Il link funziona da
16
+ * quando esiste; lo stato («in preparazione» → «pronto») lo sa già il sistema
17
+ * e adesso se lo calcola invece di farselo dire con un pulsante. `activate`
18
+ * resta nell'API e lo chiama il salvataggio quando i file sono a posto.
19
+ * «pacchetto» nel 95% dei casi ce n'è uno solo: non ha bisogno di un nome proprio. Si
20
+ * chiama «file», e l'etichetta la eredita dal nome del transfer.
21
+ *
22
+ * Il vocabolario completo (e il perché di ogni parola) sta in DELIVERY-REDESIGN.md §2.
23
+ */
24
+ import { useEffect, useRef, useState } from "react";
25
+ import { deliverySelectionName, deliverySourceFingerprint, uploadDeliveryHeroObject, } from "../lib/delivery-upload";
26
+ import { isBusy, startUpload, uploadFor, useUploadCompletions, useUploads, } from "../lib/upload-queue";
27
+ import { filesFromDrop } from "../lib/delivery-drop";
28
+ /* ============================================================================================
29
+ La colonna: elenco dei transfer
30
+ ========================================================================================== */
31
+ export function NewDeliveryButton({ onCreate, disabled }) {
32
+ return (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: disabled, onClick: onCreate, children: "Nuovo transfer" }));
33
+ }
34
+ export function DeliveriesList({ rows, current, disabled, onOpen }) {
35
+ return (_jsx("ul", { className: "col-list dl-list", children: rows.map((row) => {
36
+ const state = transferState(row);
37
+ return (_jsx("li", { children: _jsxs("button", { className: "col-item dl-row" + (current === row.id ? " is-on" : ""), disabled: disabled, onClick: () => onOpen(row.id), children: [_jsx("span", { className: "dl-state is-" + state.kind, "aria-hidden": "true" }), _jsxs("span", { className: "dl-row-main", children: [_jsx("strong", { children: row.clientName || row.title }), _jsx("span", { children: row.clientName ? row.title : formatDate(row.updatedAt) })] }), _jsx("span", { className: "dl-row-meta", children: state.label }), _jsx("span", { className: "dl-row-size", children: formatBytes(row.storedBytes) })] }) }, row.id));
38
+ }) }));
39
+ }
40
+ /* Il totale in fondo alla colonna: è l'unico posto in cui si vede quanto sta occupando tutto
41
+ insieme, ed è la domanda che precede ogni «faccio pulizia?». */
42
+ export function DeliveriesTotal({ rows }) {
43
+ if (!rows.length)
44
+ return null;
45
+ const bytes = rows.reduce((sum, row) => sum + (row.storedBytes || 0), 0);
46
+ return (_jsxs("p", { className: "dl-total", children: [rows.length, " transfer \u00B7 ", formatBytes(bytes)] }));
47
+ }
48
+ export function DeliveryWorkspace({ delivery: fromServer, busy, onAction, onRefresh, onReload, onRead, onDeleted, onOpen, onSelected, }) {
49
+ /* Il transfer «corrente» può venire dal server (l'ho aperto dall'elenco) o essere nato qui
50
+ dentro al primo file rilasciato. Nel secondo caso resta in questo stato finché non si
51
+ ricarica l'elenco. */
52
+ const [made, setMade] = useState(null);
53
+ const delivery = fromServer || made;
54
+ const [draft, setDraft] = useState(() => draftOf(fromServer));
55
+ const [clean, setClean] = useState(() => draftOf(fromServer));
56
+ const [heroes, setHeroes] = useState(fromServer?.heroes || []);
57
+ const [heroesClean, setHeroesClean] = useState(fromServer?.heroes || []);
58
+ const [saving, setSaving] = useState(false);
59
+ const [failure, setFailure] = useState(null);
60
+ const [notice, setNotice] = useState(null);
61
+ const [confirmDelete, setConfirmDelete] = useState(false);
62
+ /* Anche «Rimuovi» su un pacchetto cancella i byte all'istante: chiede conferma
63
+ * come il fondo pagina, ma dentro la riga, che è dove si è guardato. */
64
+ const [confirmPackage, setConfirmPackage] = useState(null);
65
+ const [copied, setCopied] = useState(null);
66
+ const folderRef = useRef(null);
67
+ const filesRef = useRef(null);
68
+ const coverRef = useRef(null);
69
+ const resumeRef = useRef(null);
70
+ const startingRef = useRef(false);
71
+ const link = primaryLink(delivery);
72
+ const packages = delivery?.packages || [];
73
+ const ready = packages.some((pkg) => pkg.state === "ready");
74
+ /* Il caricamento non vive più qui dentro: sta nella coda (lib/upload-queue.ts), fuori da
75
+ React, così uscire da questa schermata non lo interrompe. Qui lo si guarda e basta. */
76
+ const jobs = useUploads();
77
+ const job = delivery ? uploadFor(delivery.id, jobs) : undefined;
78
+ const uploading = isBusy(job) || startingRef.current;
79
+ const dirty = !same(draft, clean) || !sameHeroes(heroes, heroesClean);
80
+ const photos = packages.flatMap((pkg) => pkg.images.map((image) => ({ pkg, image })));
81
+ useEffect(() => {
82
+ if (!fromServer)
83
+ return;
84
+ setDraft(draftOf(fromServer));
85
+ setClean(draftOf(fromServer));
86
+ setHeroes(fromServer.heroes);
87
+ setHeroesClean(fromServer.heroes);
88
+ }, [fromServer]);
89
+ useEffect(() => {
90
+ folderRef.current?.setAttribute("webkitdirectory", "");
91
+ folderRef.current?.setAttribute("directory", "");
92
+ }, []);
93
+ /* Quando un caricamento finisce — anche mentre si stava guardando un'altra sezione — questa
94
+ schermata rilegge il proprio transfer: i file appena chiusi devono comparire senza che
95
+ nessuno ricarichi niente a mano. */
96
+ const completions = useUploadCompletions();
97
+ const seenCompletions = useRef(completions);
98
+ useEffect(() => {
99
+ if (completions === seenCompletions.current)
100
+ return;
101
+ seenCompletions.current = completions;
102
+ if (delivery)
103
+ void refreshAll(delivery.id);
104
+ }, [completions]); // eslint-disable-line react-hooks/exhaustive-deps
105
+ const change = (patch) => setDraft((current) => ({ ...current, ...patch }));
106
+ /* ---- caricamento ------------------------------------------------------------------- */
107
+ /* Qui si prepara e si consegna alla coda: transfer, pacchetto, impronta. Poi si esce
108
+ subito. Il caricamento vero, la chiusura del pacchetto e l'attivazione li fa la coda,
109
+ che non muore quando questa schermata sparisce. */
110
+ const beginUpload = async (files, resume = null) => {
111
+ if (!files.length || startingRef.current)
112
+ return;
113
+ startingRef.current = true;
114
+ setFailure(null);
115
+ setNotice(null);
116
+ let target = delivery;
117
+ let packageId = resume?.id || "";
118
+ let options = { galleryEnabled: draft.gallery, webEnabled: draft.light };
119
+ try {
120
+ /* L'impronta si legge PRIMA di creare qualsiasi cosa: se il browser ha perso il
121
+ permesso su un file, non resta un transfer a metà né un oggetto orfano. */
122
+ const fingerprint = await deliverySourceFingerprint(files);
123
+ const title = draft.title.trim() || deliverySelectionName(files) || "Transfer";
124
+ if (!target) {
125
+ change({ title });
126
+ target = (await onAction({
127
+ action: "quickCreate", title, filesExpiresAt: expiryFrom(draft.duration),
128
+ }, { reload: false })).delivery;
129
+ setMade(target);
130
+ }
131
+ if (resume) {
132
+ if (resume.sourceFingerprint !== fingerprint) {
133
+ throw new Error("Questi non sono gli stessi file di prima. Riseleziona esattamente la cartella " +
134
+ "(o i file) del caricamento interrotto.");
135
+ }
136
+ options = { galleryEnabled: resume.galleryEnabled, webEnabled: resume.webEnabled };
137
+ }
138
+ else {
139
+ const created = await onAction({
140
+ action: "createPackage", id: target.id, label: title,
141
+ ...options, sourceFingerprint: fingerprint, fileCount: files.length,
142
+ sourceBytes: files.reduce((sum, file) => sum + file.size, 0),
143
+ }, { reload: false });
144
+ packageId = created.package.id;
145
+ }
146
+ startUpload({ deliveryId: target.id, packageId, title, files, options });
147
+ await refreshAll(target.id);
148
+ }
149
+ catch (error) {
150
+ setFailure(uploadFailure(error) + (packageId
151
+ ? " Il caricamento resta qui sotto: puoi riprenderlo riselezionando gli stessi file."
152
+ : ""));
153
+ if (target)
154
+ await refreshAll(target.id).catch(() => { });
155
+ }
156
+ finally {
157
+ startingRef.current = false;
158
+ }
159
+ };
160
+ const refreshAll = async (id) => {
161
+ if (fromServer) {
162
+ await onRefresh();
163
+ return;
164
+ }
165
+ onSelected(id);
166
+ await onReload();
167
+ /* Il transfer nato qui non ha una rotta aperta: si rilegge il suo dettaglio a mano
168
+ (onRead non tocca lo stato del pannello, quindi non fa rimontare questa schermata e
169
+ non interrompe l'upload), così pacchetti, link e stato tornano quelli del server. */
170
+ const fresh = await onRead(id);
171
+ if (fresh) {
172
+ setMade(fresh);
173
+ setClean(draftOf(fresh, draft));
174
+ setHeroesClean(fresh.heroes);
175
+ }
176
+ };
177
+ const picked = (event) => {
178
+ const input = event.currentTarget;
179
+ const files = [...(input.files || [])];
180
+ const resume = resumeRef.current;
181
+ resumeRef.current = null;
182
+ /* Safari può revocare i riferimenti File appena si azzera l'input, anche se la FileList
183
+ è già stata copiata: si svuota SOLO a lettura finita. */
184
+ if (files.length)
185
+ void beginUpload(files, resume).finally(() => { input.value = ""; });
186
+ };
187
+ const choose = (input, resume = null) => {
188
+ resumeRef.current = resume;
189
+ if (input)
190
+ input.value = "";
191
+ input?.click();
192
+ };
193
+ /* ---- salvataggio: uno solo, che fa tutto ------------------------------------------- */
194
+ const save = async () => {
195
+ if (!delivery)
196
+ return;
197
+ setSaving(true);
198
+ setFailure(null);
199
+ setNotice(null);
200
+ try {
201
+ const updated = (await onAction({
202
+ action: "update", id: delivery.id, title: draft.title.trim() || delivery.title,
203
+ clientName: draft.clientName, description: draft.description,
204
+ licenseNotes: draft.licenseNotes, ctaEnabled: draft.ctaEnabled,
205
+ ctaLabel: draft.ctaLabel, ctaUrl: draft.ctaUrl, syncPackageLabel: true,
206
+ expiresAt: null,
207
+ filesExpiresAt: draft.duration === "keep"
208
+ ? delivery.filesExpiresAt : expiryFrom(draft.duration),
209
+ }, { reload: false })).delivery;
210
+ if (!sameHeroes(heroes, heroesClean)) {
211
+ await onAction({ action: "commitHeroes", id: delivery.id, heroes }, { reload: false });
212
+ }
213
+ if (link) {
214
+ if ((link.pin || "") !== draft.pin) {
215
+ await onAction({
216
+ action: "updateLink", id: delivery.id, linkId: link.id,
217
+ label: link.label, pin: draft.pin, expiresAt: link.expiresAt,
218
+ }, { reload: false });
219
+ }
220
+ }
221
+ else {
222
+ await onAction({
223
+ action: "createLink", id: delivery.id, label: "Link del cliente", pin: draft.pin,
224
+ }, { reload: false });
225
+ }
226
+ if (ready)
227
+ await onAction({ action: "activate", id: delivery.id }, { reload: false }).catch(() => { });
228
+ setClean({ ...draft, duration: "keep" });
229
+ setDraft((current) => ({ ...current, duration: "keep" }));
230
+ setHeroesClean(heroes);
231
+ setNotice(link ? "Salvato" : "Link pronto: ora puoi mandarlo");
232
+ await refreshAll(updated.id);
233
+ }
234
+ catch (error) {
235
+ setFailure(errorText(error));
236
+ }
237
+ finally {
238
+ setSaving(false);
239
+ }
240
+ };
241
+ /* ---- copertina --------------------------------------------------------------------- */
242
+ const toggleCover = (packageId, index, previewUrl) => {
243
+ setHeroes((current) => {
244
+ const has = current.some((item) => item.type === "package" && item.packageId === packageId && item.index === index);
245
+ if (has) {
246
+ return current.filter((item) => !(item.type === "package" && item.packageId === packageId && item.index === index));
247
+ }
248
+ if (current.length >= 8)
249
+ return current;
250
+ return [...current, { type: "package", packageId, index, previewUrl }];
251
+ });
252
+ };
253
+ const uploadCovers = async (event) => {
254
+ const input = event.currentTarget;
255
+ const files = [...(input.files || [])].filter((f) => /\.(jpe?g|png|webp)$/i.test(f.name)).slice(0, 8);
256
+ const pkg = packages.find((item) => item.state === "ready");
257
+ if (!files.length || !pkg || !delivery) {
258
+ input.value = "";
259
+ setFailure(pkg ? "Scegli almeno un'immagine JPEG, PNG o WebP" : "Prima carica i file del transfer");
260
+ return;
261
+ }
262
+ setSaving(true);
263
+ setFailure(null);
264
+ try {
265
+ const added = [];
266
+ for (const file of files) {
267
+ const uploaded = await uploadDeliveryHeroObject(delivery.id, pkg.id, file);
268
+ added.push({ ...uploaded, previewUrl: "" });
269
+ }
270
+ setHeroes(added);
271
+ }
272
+ catch (error) {
273
+ setFailure(errorText(error));
274
+ }
275
+ finally {
276
+ setSaving(false);
277
+ input.value = "";
278
+ }
279
+ };
280
+ /* ---- azioni di servizio ------------------------------------------------------------ */
281
+ const run = async (payload, done) => {
282
+ setFailure(null);
283
+ setNotice(null);
284
+ try {
285
+ await onAction(payload, { reload: false });
286
+ if (delivery)
287
+ await refreshAll(delivery.id);
288
+ setNotice(done);
289
+ }
290
+ catch (error) {
291
+ setFailure(errorText(error));
292
+ }
293
+ };
294
+ /* «Usa come modello» copia impostazioni e testi in un transfer nuovo, e ci porta dentro:
295
+ restare su quello vecchio dopo aver chiesto una copia non ha senso. */
296
+ const duplicate = async () => {
297
+ setFailure(null);
298
+ setNotice(null);
299
+ try {
300
+ const out = await onAction({ action: "duplicate", id: delivery?.id }, { reload: false });
301
+ await onReload();
302
+ onOpen(out.delivery.id);
303
+ }
304
+ catch (error) {
305
+ setFailure(errorText(error));
306
+ }
307
+ };
308
+ const remove = async () => {
309
+ setFailure(null);
310
+ try {
311
+ await onAction({ action: "delete", id: delivery?.id }, { reload: false });
312
+ await onReload();
313
+ onDeleted();
314
+ }
315
+ catch (error) {
316
+ setFailure(errorText(error));
317
+ }
318
+ };
319
+ const copy = async (value, what) => {
320
+ await navigator.clipboard.writeText(value);
321
+ setCopied(what);
322
+ setTimeout(() => setCopied(null), 2000);
323
+ };
324
+ const state = delivery ? transferState(delivery) : null;
325
+ const expiry = draft.duration === "keep"
326
+ ? delivery?.filesExpiresAt || null : expiryFrom(draft.duration);
327
+ return (_jsxs("div", { className: "dl-work", children: [_jsxs("header", { className: "dl-work-head", children: [_jsxs("div", { className: "dl-work-title", children: [state && _jsx("span", { className: "dl-pill is-" + state.kind, children: state.label }), _jsx("h2", { children: draft.title.trim() || (delivery ? delivery.title : "Nuovo transfer") })] }), _jsxs("div", { className: "dl-work-actions", children: [link && (_jsx("a", { className: "ak-btn", href: link.url, target: "_blank", rel: "noreferrer", children: "Guarda come lo vede il cliente" })), link && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: !dirty || saving || busy, onClick: () => void save(), children: saving ? "Salvo…" : "Salva" }))] })] }), (failure || notice) && (_jsxs("p", { className: failure ? "ak-error dl-feedback" : "dl-feedback is-ok", children: [failure || notice, _jsx("button", { type: "button", "aria-label": "Chiudi", onClick: () => { setFailure(null); setNotice(null); }, children: "\u00D7" })] })), _jsxs("div", { className: "dl-grid", children: [_jsxs("div", { className: "dl-main", children: [_jsxs("section", { className: "dl-card", children: [_jsxs("div", { className: "dl-pick", children: [_jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(filesRef.current), children: [_jsx(FileIcon, {}), "Aggiungi file"] }), _jsxs("button", { className: "dl-pick-tile", disabled: uploading, onClick: () => choose(folderRef.current), children: [_jsx(FolderIcon, {}), "Aggiungi una cartella"] }), _jsx("input", { ref: folderRef, type: "file", multiple: true, hidden: true, onChange: picked }), _jsx("input", { ref: filesRef, type: "file", multiple: true, hidden: true, onChange: picked })] }), _jsx(DropArea, { disabled: uploading, resuming: !!resumeRef.current, onFiles: (files) => {
328
+ const resume = resumeRef.current;
329
+ resumeRef.current = null;
330
+ void beginUpload(files, resume);
331
+ }, onError: (error) => setFailure(errorText(error)) }), job && isBusy(job) && _jsx(Progress, { job: job }), !!packages.length && (_jsx("ul", { className: "dl-files", children: packages.map((pkg) => (_jsxs("li", { className: "dl-file is-" + pkg.state + (confirmPackage === pkg.id ? " is-confirming" : ""), children: [_jsx("span", { className: "dl-file-icon", "aria-hidden": "true", children: pkg.state === "ready" ? "✓" : "…" }), confirmPackage === pkg.id ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: ["Rimuovo ", pkg.fileCount || "queste", " foto?"] }), _jsx("span", { children: "Spariscono adesso, e non si torna indietro." })] }), _jsxs("span", { className: "dl-file-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => setConfirmPackage(null), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", disabled: uploading, onClick: () => {
332
+ setConfirmPackage(null);
333
+ void run({ action: "deletePackage", id: delivery?.id, packageId: pkg.id }, "File rimossi");
334
+ }, children: "S\u00EC, rimuovi" })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "dl-file-main", children: [_jsxs("strong", { children: [pkg.fileCount || "—", " foto \u00B7 ", formatBytes(pkg.sourceBytes)] }), _jsx("span", { children: pkg.state === "ready"
335
+ ? [pkg.galleryEnabled ? "si vedono sulla pagina" : "", pkg.webEnabled ? "versione leggera" : ""]
336
+ .filter(Boolean).join(" · ") || "solo originali"
337
+ : job?.id === pkg.id && isBusy(job)
338
+ ? (job.state === "paused" ? "caricamento in pausa" : "caricamento in corso")
339
+ : "caricamento interrotto" })] }), _jsxs("span", { className: "dl-file-actions", children: [pkg.state !== "ready" && !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn", disabled: uploading, onClick: () => { resumeRef.current = pkg; setNotice(resumeHint); }, children: "Riprendi" })), !(job?.id === pkg.id && isBusy(job)) && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: uploading, onClick: () => setConfirmPackage(pkg.id), children: "Rimuovi" }))] })] }))] }, pkg.id))) }))] }), _jsxs("section", { className: "dl-card dl-about", children: [_jsx(Field, { label: "Nome del transfer", hint: "Lo vede il cliente, ed \u00E8 il nome dello ZIP che scarica.", children: _jsx("input", { value: draft.title, placeholder: "Es. Matrimonio Rossi", onChange: (e) => change({ title: e.target.value }) }) }), _jsx(Field, { label: "Per", hint: "Il nome del cliente, grande in cima alla pagina.", children: _jsx("input", { value: draft.clientName, placeholder: "Es. Marta Rossi", onChange: (e) => change({ clientName: e.target.value }) }) }), _jsx(Field, { label: "Messaggio", wide: true, hint: "Due righe di accompagnamento. Si pu\u00F2 lasciare vuoto.", children: _jsx("textarea", { rows: 3, value: draft.description, onChange: (e) => change({ description: e.target.value }) }) })] }), link ? (_jsxs("section", { className: "dl-card dl-link", children: [_jsxs("div", { className: "dl-link-head", children: [_jsx("span", { className: "dl-step", children: "Il link per il cliente" }), _jsx("code", { children: link.url })] }), _jsxs("div", { className: "dl-link-actions", children: [_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => void copy(link.url, "link"), children: copied === "link" ? "Copiato" : "Copia il link" }), _jsx("button", { className: "ak-btn", onClick: () => void copy(messageFor(draft, link, expiry), "messaggio"), children: copied === "messaggio" ? "Copiato" : "Copia il messaggio" }), _jsx("a", { className: "ak-btn", target: "_blank", rel: "noreferrer", href: `https://wa.me/?text=${encodeURIComponent(messageFor(draft, link, expiry))}`, children: "Apri in WhatsApp" })] }), _jsx("p", { className: "dl-link-stats", children: linkStory(link) }), draft.pin && (_jsxs("p", { className: "dl-link-stats", children: ["Codice di accesso ", _jsx("strong", { children: draft.pin }), " \u2014 \u00E8 gi\u00E0 dentro il messaggio."] }))] })) : (_jsxs("section", { className: "dl-card dl-get", children: [_jsx("button", { className: "ak-btn ak-btn-primary dl-get-btn", disabled: !delivery || saving || busy, onClick: () => void save(), children: saving ? "Preparo…" : "Ottieni il link" }), _jsx("p", { children: delivery
340
+ ? "Il link vale già mentre l'upload continua: chi lo apre vede che le foto stanno arrivando."
341
+ : "Aggiungi i file: il caricamento parte subito e il resto si compila mentre va." })] }))] }), _jsxs("div", { className: "dl-side", children: [_jsxs(Group, { title: "Chi pu\u00F2 aprirlo", children: [_jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !draft.pin, onChange: () => change({ pin: "" }) }), _jsxs("span", { children: [_jsx("strong", { children: "Chiunque abbia il link" }), _jsx("small", { children: "Il link \u00E8 lungo e casuale: non si indovina." })] })] }), _jsxs("label", { className: "dl-radio", children: [_jsx("input", { type: "radio", name: "dl-access", checked: !!draft.pin, onChange: () => change({ pin: draft.pin || makeCode() }) }), _jsxs("span", { children: [_jsx("strong", { children: "Serve anche un codice" }), _jsx("small", { children: "Da mandare insieme al link. Dopo dieci errori si blocca per un'ora." })] })] }), !!draft.pin && (_jsxs("div", { className: "dl-code", children: [_jsx("input", { value: draft.pin, spellCheck: false, onChange: (e) => change({ pin: e.target.value.toUpperCase() }) }), _jsx("button", { className: "ak-btn", onClick: () => change({ pin: makeCode() }), children: "Altro" })] }))] }), _jsxs(Group, { title: "Cosa riceve", note: packages.length
342
+ ? "Si decide prima di caricare: per cambiarle bisognerebbe ricaricare i file."
343
+ : undefined, children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.gallery, disabled: !!packages.length, onChange: (e) => change({ gallery: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Vede le foto sulla pagina" }), _jsx("small", { children: "Anteprime e ingrandimento, senza scaricare niente. Da telefono \u00E8 la differenza fra un link e una consegna." })] })] }), _jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.light, disabled: !!packages.length, onChange: (e) => change({ light: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Pu\u00F2 scaricare una versione leggera" }), _jsx("small", { children: "Un secondo download, adatto a social e web. Gli originali ci sono sempre." })] })] })] }), _jsxs(Group, { title: "Per quanto resta scaricabile", children: [_jsxs("select", { className: "dl-select", value: draft.duration, onChange: (e) => change({ duration: e.target.value }), children: [delivery && _jsx("option", { value: "keep", children: delivery.filesExpiresAt
344
+ ? `Fino al ${formatDate(delivery.filesExpiresAt)}`
345
+ : "Per sempre" }), _jsx("option", { value: "7", children: "7 giorni" }), _jsx("option", { value: "30", children: "30 giorni" }), _jsx("option", { value: "180", children: "6 mesi" }), _jsx("option", { value: "365", children: "1 anno" }), _jsx("option", { value: "forever", children: "Per sempre" })] }), _jsx("p", { className: "dl-note", children: expiry
346
+ ? `Dopo il ${formatDate(expiry)} il link continua ad aprirsi, ma i file non si scaricano più.`
347
+ : "I file restano finché non li togli tu." })] }), _jsxs(Group, { title: "Alla fine della pagina", children: [_jsxs("label", { className: "dl-check", children: [_jsx("input", { type: "checkbox", checked: draft.ctaEnabled, onChange: (e) => change({ ctaEnabled: e.target.checked }) }), _jsxs("span", { children: [_jsx("strong", { children: "Chiedi una recensione" }), _jsx("small", { children: "Un pulsante in fondo, dopo le fotografie." })] })] }), draft.ctaEnabled && (_jsxs(_Fragment, { children: [_jsx(Field, { label: "Testo del pulsante", children: _jsx("input", { value: draft.ctaLabel, onChange: (e) => change({ ctaLabel: e.target.value }) }) }), _jsx(Field, { label: "Indirizzo", hint: "Il link alla tua pagina delle recensioni.", children: _jsx("input", { type: "url", value: draft.ctaUrl, placeholder: "https://\u2026", onChange: (e) => change({ ctaUrl: e.target.value }) }) })] })), _jsx(Field, { label: "Note d'uso", hint: "Cosa pu\u00F2 farci il cliente. Appaiono in fondo alla pagina.", children: _jsx("textarea", { rows: 3, value: draft.licenseNotes, onChange: (e) => change({ licenseNotes: e.target.value }) }) })] })] })] }), !!delivery && (_jsxs("section", { className: "dl-card dl-cover", children: [_jsxs("div", { className: "dl-cover-head", children: [_jsxs("div", { children: [_jsx("span", { className: "dl-step", children: "Copertina" }), _jsxs("p", { children: ["Le foto grandi in cima alla pagina del cliente, fino a otto.", " ", heroes.length
348
+ ? `Ne hai scelte ${heroes.length}.`
349
+ : "Nessuna scelta: si userà quella predefinita."] })] }), _jsxs("div", { className: "dl-cover-actions", children: [_jsx("button", { className: "ak-btn", disabled: saving || !ready, onClick: () => { if (coverRef.current)
350
+ coverRef.current.value = ""; coverRef.current?.click(); }, children: "Carica un'immagine" }), !!heroes.length && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setHeroes([]), children: "Usa quella predefinita" })), _jsx("input", { ref: coverRef, type: "file", accept: "image/jpeg,image/png,image/webp", multiple: true, hidden: true, onChange: (event) => void uploadCovers(event) })] })] }), photos.length ? (_jsx("div", { className: "dl-cover-grid", children: photos.map(({ pkg, image }) => {
351
+ const chosen = heroes.some((item) => item.type === "package" && item.packageId === pkg.id && item.index === image.index);
352
+ return (_jsxs("button", { type: "button", className: chosen ? "is-chosen" : "", "aria-pressed": chosen, onClick: () => toggleCover(pkg.id, image.index, image.previewUrl), children: [_jsx("img", { src: image.previewUrl, alt: image.name, loading: "lazy" }), chosen && _jsx("em", { "aria-hidden": "true", children: "\u2713" })] }, `${pkg.id}-${image.index}`));
353
+ }) })) : (_jsx("p", { className: "dl-note dl-cover-empty", children: heroes.some((item) => item.type === "object")
354
+ ? `${heroes.length} immagini caricate a mano.`
355
+ : "Questo transfer non ha anteprime da cui scegliere: carica un'immagine, oppure resta quella predefinita." }))] })), !!delivery && (_jsxs("section", { className: "dl-danger", children: [_jsxs("div", { children: [_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void duplicate(), children: "Usa come modello" }), delivery.status === "active" && (_jsx("button", { className: "ak-btn ak-btn-quiet", disabled: busy, onClick: () => void run({ action: "archive", id: delivery.id }, "Originali tolti: recuperabili per sette giorni"), children: "Libera spazio: togli gli originali" }))] }), confirmDelete ? (_jsxs("div", { className: "dl-confirm", children: [_jsxs("p", { children: ["Elimino ", _jsx("strong", { children: delivery.title }), photos.length ? ` (${countPhotos(packages)} foto, ${formatBytes(delivery.storedBytes)})` : "", "? Il link smette subito di funzionare e i file vengono cancellati adesso. Non si torna indietro."] }), _jsx("button", { className: "ak-btn", onClick: () => setConfirmDelete(false), children: "Annulla" }), _jsx("button", { className: "ak-btn dl-danger-btn", onClick: () => void remove(), children: "S\u00EC, elimina" })] })) : (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setConfirmDelete(true), children: "Elimina il transfer" }))] }))] }));
356
+ }
357
+ /* ============================================================================================
358
+ Impostazioni
359
+ ========================================================================================== */
360
+ export function DeliverySettings({ settings, onAction, pickMedia }) {
361
+ const [covers, setCovers] = useState(settings.defaultHeroes);
362
+ const [notice, setNotice] = useState(null);
363
+ useEffect(() => setCovers(settings.defaultHeroes), [settings]);
364
+ return (_jsxs("div", { className: "dl-work", children: [_jsxs("header", { className: "dl-work-head", children: [_jsx("div", { className: "dl-work-title", children: _jsx("h2", { children: "Copertina predefinita" }) }), _jsx("div", { className: "dl-work-actions", children: _jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => void onAction({
365
+ action: "saveSettings", defaultHeroes: covers,
366
+ }).then(() => setNotice("Salvata")), children: "Salva" }) })] }), notice && _jsx("p", { className: "dl-feedback is-ok", children: notice }), _jsxs("section", { className: "dl-card dl-cover", children: [_jsxs("div", { className: "dl-cover-head", children: [_jsxs("div", { children: [_jsx("span", { className: "dl-step", children: "Pagina del cliente" }), _jsx("p", { children: "Queste immagini vanno in cima ai transfer per cui non scegli una copertina." })] }), _jsxs("div", { className: "dl-cover-actions", children: [_jsx("button", { className: "ak-btn", onClick: () => void pickMedia({ multiple: true })
367
+ .then((chosen) => { if (chosen)
368
+ setCovers(chosen.slice(0, 8)); }), children: "Scegli dalla libreria" }), !!covers.length && (_jsx("button", { className: "ak-btn ak-btn-quiet", onClick: () => setCovers([]), children: "Nessuna" }))] })] }), !!covers.length && (_jsx("div", { className: "dl-cover-grid is-static", children: covers.map((url) => _jsx("img", { src: url, alt: "" }, url)) }))] })] }));
369
+ }
370
+ /* ============================================================================================
371
+ Pezzi
372
+ ========================================================================================== */
373
+ /* I due segni delle mattonelle. Disegnati qui e non in ui/icons.tsx: non li usa nessun altro,
374
+ e la scelta «file o cartella» è di questa schermata. */
375
+ function FileIcon() {
376
+ return (_jsxs("svg", { viewBox: "0 0 24 24", width: "22", height: "22", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" }), _jsx("path", { d: "M14 3v5h5" }), _jsx("path", { d: "M12 11v6M9 14h6" })] }));
377
+ }
378
+ function FolderIcon() {
379
+ return (_jsxs("svg", { viewBox: "0 0 24 24", width: "22", height: "22", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [_jsx("path", { d: "M3 7a2 2 0 0 1 2-2h4l2 2.5h8a2 2 0 0 1 2 2V18a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }), _jsx("path", { d: "M12 11.5v5M9.5 14h5" })] }));
380
+ }
381
+ function Group({ title, note, children }) {
382
+ return (_jsxs("section", { className: "dl-group", children: [_jsx("h3", { children: title }), children, note && _jsx("p", { className: "dl-note", children: note })] }));
383
+ }
384
+ function Field({ label, hint, wide, children }) {
385
+ return (_jsxs("label", { className: "dl-field" + (wide ? " is-wide" : ""), children: [_jsx("span", { children: label }), children, hint && _jsx("small", { children: hint })] }));
386
+ }
387
+ function DropArea({ onFiles, onError, disabled, resuming }) {
388
+ const [over, setOver] = useState(false);
389
+ const drop = async (event) => {
390
+ event.preventDefault();
391
+ event.stopPropagation();
392
+ setOver(false);
393
+ if (disabled)
394
+ return;
395
+ try {
396
+ const files = await filesFromDrop(event.dataTransfer);
397
+ if (!files.length)
398
+ throw new Error("Nel rilascio non c'era nessun file");
399
+ onFiles(files);
400
+ }
401
+ catch (error) {
402
+ /* L'errore resta qui: non è stato creato né caricato niente, quindi un nuovo
403
+ rilascio riparte pulito. */
404
+ onError?.(error);
405
+ }
406
+ };
407
+ return (_jsxs("div", { className: "dl-drop" + (over ? " is-over" : "") + (disabled ? " is-off" : ""), onDragEnter: (e) => { e.preventDefault(); e.stopPropagation(); if (!disabled)
408
+ setOver(true); }, onDragOver: (e) => { e.preventDefault(); e.stopPropagation(); }, onDragLeave: (e) => {
409
+ e.preventDefault();
410
+ e.stopPropagation();
411
+ if (!e.currentTarget.contains(e.relatedTarget))
412
+ setOver(false);
413
+ }, onDrop: (event) => void drop(event), children: [_jsx("strong", { children: resuming ? "Rilascia qui gli stessi file di prima" : "…oppure trascinali qui" }), _jsx("span", { children: "Anche pi\u00F9 cartelle insieme: la struttura viene conservata nello ZIP." })] }));
414
+ }
415
+ function Progress({ job }) {
416
+ const progress = job.progress;
417
+ if (!progress) {
418
+ return (_jsxs("div", { className: "dl-progress", children: [_jsx("div", { className: "dl-progress-top", children: _jsx("strong", { children: "Preparo il caricamento\u2026" }) }), _jsx("div", { className: "dl-progress-track", children: _jsx("i", { style: { width: "3%" } }) })] }));
419
+ }
420
+ const done = progress.totalFiles ? Math.min(1, progress.doneFiles / progress.totalFiles) : 0;
421
+ const phase = job.state === "paused" ? "In pausa"
422
+ : progress.phase === "originals" ? "Carico le foto"
423
+ : progress.phase === "derivatives" ? "Preparo le anteprime"
424
+ : "Ultimi controlli";
425
+ const seconds = progress.transferStartedAt
426
+ ? Math.max(1, (Date.now() - progress.transferStartedAt) / 1000) : 0;
427
+ const mbps = seconds ? progress.transferredBytes * 8 / seconds / 1_000_000 : 0;
428
+ const left = progress.phase === "originals" && mbps > 0
429
+ ? Math.max(0, (progress.sourceBytes - progress.uploadedBytes) * 8 / (mbps * 1_000_000))
430
+ : null;
431
+ return (_jsxs("div", { className: "dl-progress", children: [_jsxs("div", { className: "dl-progress-top", children: [_jsx("strong", { children: phase }), _jsxs("span", { children: [progress.doneFiles, " di ", progress.totalFiles, left !== null && left > 5 ? ` · ancora ${formatDuration(left)}` : ""] })] }), _jsx("div", { className: "dl-progress-track", children: _jsx("i", { style: { width: `${Math.max(2, done * 100)}%` } }) }), _jsxs("p", { children: [progress.file || "…", " \u00B7 ", formatBytes(progress.uploadedBytes), mbps > 0 ? ` · ${mbps.toFixed(1)} Mbps` : "", progress.note ? ` · ${progress.note}` : ""] }), _jsx("small", { children: "Puoi andare dove vuoi nel pannello: il caricamento continua e lo ritrovi nel riquadro in basso a destra. Se chiudi il browser si ferma, e lo riprendi da qui." })] }));
432
+ }
433
+ /* ============================================================================================
434
+ Calcoli
435
+ ========================================================================================== */
436
+ const resumeHint = "Riseleziona la stessa cartella (o gli stessi file) e rilasciali qui sotto: " +
437
+ "quello che è già stato caricato non viene ricaricato.";
438
+ function draftOf(delivery, keep) {
439
+ if (!delivery) {
440
+ return {
441
+ title: "", clientName: "", description: "", licenseNotes: "",
442
+ ctaEnabled: false, ctaLabel: "Lascia una recensione", ctaUrl: "",
443
+ duration: "30", pin: "", gallery: true, light: true,
444
+ };
445
+ }
446
+ const link = primaryLink(delivery);
447
+ return {
448
+ title: delivery.title,
449
+ clientName: delivery.clientName,
450
+ description: delivery.description,
451
+ licenseNotes: delivery.licenseNotes,
452
+ ctaEnabled: delivery.ctaEnabled,
453
+ ctaLabel: delivery.ctaLabel || "Lascia una recensione",
454
+ ctaUrl: delivery.ctaUrl,
455
+ duration: "keep",
456
+ pin: link?.pin || "",
457
+ /* Gallery e versione leggera non sono campi del transfer ma del caricamento: per un
458
+ transfer che ha già i file, la verità è quella dei file. */
459
+ gallery: delivery.packages[0]?.galleryEnabled ?? keep?.gallery ?? true,
460
+ light: delivery.packages[0]?.webEnabled ?? keep?.light ?? true,
461
+ };
462
+ }
463
+ function same(a, b) {
464
+ return Object.keys(a).every((key) => a[key] === b[key]);
465
+ }
466
+ function sameHeroes(a, b) {
467
+ if (a.length !== b.length)
468
+ return false;
469
+ return a.every((item, index) => heroKey(item) === heroKey(b[index]));
470
+ }
471
+ function heroKey(item) {
472
+ return item.type === "package" ? `p:${item.packageId}:${item.index}` : `o:${item.id}`;
473
+ }
474
+ function primaryLink(delivery) {
475
+ return delivery?.links.find((item) => !item.revokedAt) || null;
476
+ }
477
+ function transferState(row) {
478
+ if (row.filesExpiresAt) {
479
+ const days = (new Date(row.filesExpiresAt).getTime() - Date.now()) / 86_400_000;
480
+ if (days < 0)
481
+ return { kind: "expired", label: "File scaduti" };
482
+ if (row.status === "active" && days <= 7)
483
+ return { kind: "expiring", label: "In scadenza" };
484
+ }
485
+ if (row.status === "uploading")
486
+ return { kind: "uploading", label: "In preparazione" };
487
+ if (row.status === "archived")
488
+ return { kind: "archived", label: "Originali tolti" };
489
+ if (row.status === "active") {
490
+ return row.downloadCount > 0
491
+ ? { kind: "downloaded", label: "Scaricato" }
492
+ : { kind: "ready", label: "Pronto" };
493
+ }
494
+ return { kind: "draft", label: "Senza file" };
495
+ }
496
+ function countPhotos(packages) {
497
+ return packages.reduce((sum, pkg) => sum + (pkg.fileCount || 0), 0);
498
+ }
499
+ /* Il codice si genera, non si inventa: niente 0/O e 1/I, che al telefono si sbagliano. */
500
+ function makeCode() {
501
+ const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
502
+ const pick = () => Array.from(crypto.getRandomValues(new Uint8Array(4)))
503
+ .map((n) => alphabet[n % alphabet.length]).join("");
504
+ return `${pick()}-${pick()}`;
505
+ }
506
+ function messageFor(draft, link, expiry) {
507
+ const who = draft.clientName.trim().split(/\s+/)[0];
508
+ return [
509
+ `Ciao${who ? ` ${who}` : ""}, le fotografie di ${draft.title.trim() || "questo lavoro"} sono pronte:`,
510
+ link.url,
511
+ draft.pin ? `Codice di accesso: ${draft.pin}` : "",
512
+ expiry ? `Puoi scaricarle fino al ${formatDate(expiry)}.` : "",
513
+ ].filter(Boolean).join("\n");
514
+ }
515
+ function linkStory(link) {
516
+ if (!link.openedCount)
517
+ return "Non è ancora stato aperto.";
518
+ const opened = `Aperto ${link.openedCount === 1 ? "una volta" : `${link.openedCount} volte`}`;
519
+ const when = link.lastOpenedAt ? `, l’ultima il ${formatDate(link.lastOpenedAt)}` : "";
520
+ const got = link.downloadCount ? "ha scaricato" : "non ha ancora scaricato";
521
+ return `${opened}${when} · ${got}.`;
522
+ }
523
+ function expiryFrom(duration) {
524
+ if (duration === "forever" || duration === "keep")
525
+ return null;
526
+ const days = Number(duration);
527
+ return new Date(Date.now() + (Number.isFinite(days) ? days : 30) * 86_400_000).toISOString();
528
+ }
529
+ function formatBytes(value) {
530
+ if (!value)
531
+ return "0 B";
532
+ const units = ["B", "KB", "MB", "GB", "TB"];
533
+ let n = value, i = 0;
534
+ while (n >= 1024 && i < units.length - 1) {
535
+ n /= 1024;
536
+ i++;
537
+ }
538
+ return `${n < 10 && i ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
539
+ }
540
+ function formatDate(value) {
541
+ return new Intl.DateTimeFormat("it-IT", { day: "numeric", month: "short", year: "numeric" })
542
+ .format(new Date(value));
543
+ }
544
+ function formatDuration(seconds) {
545
+ if (seconds < 60)
546
+ return `${Math.ceil(seconds)} secondi`;
547
+ if (seconds < 3600)
548
+ return `${Math.ceil(seconds / 60)} minuti`;
549
+ const hours = Math.floor(seconds / 3600);
550
+ return `${hours} h ${Math.ceil((seconds % 3600) / 60)} min`;
551
+ }
552
+ function errorText(error) {
553
+ return error instanceof Error ? error.message : String(error);
554
+ }
555
+ function uploadFailure(error) {
556
+ const raw = errorText(error);
557
+ if ((error instanceof DOMException && error.name === "NotReadableError") ||
558
+ raw.includes("requested file could not be read")) {
559
+ return "Il browser non riesce più a leggere uno dei file scelti. " +
560
+ "Riseleziona la stessa cartella e premi «Riprendi».";
561
+ }
562
+ return raw;
563
+ }
@@ -0,0 +1,12 @@
1
+ import type { AssetFile } from "../../index.js";
2
+ import type { GalleryImage, PageSection, SectionType } from "../../index.js";
3
+ export type GallerySection = PageSection;
4
+ export type GallerySectionType = SectionType;
5
+ export declare function GallerySectionsEditor({ value, onChange, renderGallery, pickMediaFiles, }: {
6
+ value: unknown;
7
+ onChange: (next: GallerySection[]) => void;
8
+ renderGallery?: (images: GalleryImage[], onImages: (next: GalleryImage[]) => void) => React.ReactNode;
9
+ pickMediaFiles?: (o: {
10
+ multiple: boolean;
11
+ }) => Promise<AssetFile[] | null>;
12
+ }): import("react").JSX.Element;