@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.
- package/dist/api.d.ts +195 -0
- package/dist/api.js +118 -0
- package/dist/app/App.d.ts +3 -0
- package/dist/app/App.js +320 -0
- package/dist/app/app.css +2068 -0
- package/dist/app/index.d.ts +18 -0
- package/dist/app/index.js +33 -0
- package/dist/app/lib/bookings.d.ts +132 -0
- package/dist/app/lib/bookings.js +79 -0
- package/dist/app/lib/deliveries.d.ts +95 -0
- package/dist/app/lib/deliveries.js +104 -0
- package/dist/app/lib/delivery-drop.d.ts +1 -0
- package/dist/app/lib/delivery-drop.js +72 -0
- package/dist/app/lib/delivery-upload.d.ts +78 -0
- package/dist/app/lib/delivery-upload.js +585 -0
- package/dist/app/lib/groups.d.ts +9 -0
- package/dist/app/lib/groups.js +32 -0
- package/dist/app/lib/home.d.ts +11 -0
- package/dist/app/lib/home.js +67 -0
- package/dist/app/lib/overview.d.ts +82 -0
- package/dist/app/lib/overview.js +68 -0
- package/dist/app/lib/pages.d.ts +24 -0
- package/dist/app/lib/pages.js +156 -0
- package/dist/app/lib/publish.d.ts +18 -0
- package/dist/app/lib/publish.js +63 -0
- package/dist/app/lib/reorder.d.ts +3 -0
- package/dist/app/lib/reorder.js +31 -0
- package/dist/app/lib/requests.d.ts +10 -0
- package/dist/app/lib/requests.js +67 -0
- package/dist/app/lib/router.d.ts +1 -0
- package/dist/app/lib/router.js +14 -0
- package/dist/app/lib/settings.d.ts +29 -0
- package/dist/app/lib/settings.js +82 -0
- package/dist/app/lib/slug.d.ts +3 -0
- package/dist/app/lib/slug.js +23 -0
- package/dist/app/lib/text.d.ts +1 -0
- package/dist/app/lib/text.js +13 -0
- package/dist/app/lib/theme.d.ts +7 -0
- package/dist/app/lib/theme.js +43 -0
- package/dist/app/lib/upload-queue.d.ts +35 -0
- package/dist/app/lib/upload-queue.js +167 -0
- package/dist/app/routes/AboutEditor.d.ts +15 -0
- package/dist/app/routes/AboutEditor.js +122 -0
- package/dist/app/routes/Bookings.d.ts +21 -0
- package/dist/app/routes/Bookings.js +134 -0
- package/dist/app/routes/Deliveries.d.ts +37 -0
- package/dist/app/routes/Deliveries.js +563 -0
- package/dist/app/routes/GallerySectionsEditor.d.ts +12 -0
- package/dist/app/routes/GallerySectionsEditor.js +107 -0
- package/dist/app/routes/HomeEditor.d.ts +10 -0
- package/dist/app/routes/HomeEditor.js +188 -0
- package/dist/app/routes/Overview.d.ts +17 -0
- package/dist/app/routes/Overview.js +318 -0
- package/dist/app/routes/PageActions.d.ts +12 -0
- package/dist/app/routes/PageActions.js +93 -0
- package/dist/app/routes/PageEditor.d.ts +23 -0
- package/dist/app/routes/PageEditor.js +233 -0
- package/dist/app/routes/PageSeoEditor.d.ts +13 -0
- package/dist/app/routes/PageSeoEditor.js +102 -0
- package/dist/app/routes/PagesList.d.ts +10 -0
- package/dist/app/routes/PagesList.js +76 -0
- package/dist/app/routes/PortfolioWorksEditor.d.ts +12 -0
- package/dist/app/routes/PortfolioWorksEditor.js +139 -0
- package/dist/app/routes/Requests.d.ts +13 -0
- package/dist/app/routes/Requests.js +30 -0
- package/dist/app/routes/SiteSettingsEditor.d.ts +21 -0
- package/dist/app/routes/SiteSettingsEditor.js +66 -0
- package/dist/app/routes/UploadDock.d.ts +1 -0
- package/dist/app/routes/UploadDock.js +66 -0
- package/dist/app/routes/WorkshopEditionsEditor.d.ts +12 -0
- package/dist/app/routes/WorkshopEditionsEditor.js +101 -0
- package/dist/app/site.d.ts +37 -0
- package/dist/app/site.js +18 -0
- package/dist/app/ui/MediaSlot.d.ts +10 -0
- package/dist/app/ui/MediaSlot.js +20 -0
- package/dist/app/ui/OpenCard.d.ts +18 -0
- package/dist/app/ui/OpenCard.js +20 -0
- package/dist/app/ui/ParagraphEditor.d.ts +8 -0
- package/dist/app/ui/ParagraphEditor.js +37 -0
- package/dist/app/ui/dnd.d.ts +1 -0
- package/dist/app/ui/dnd.js +11 -0
- package/dist/app/ui/icons.d.ts +26 -0
- package/dist/app/ui/icons.js +45 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +26 -0
- package/dist/form/MediaBrowser.d.ts +13 -0
- package/dist/form/MediaBrowser.js +24 -0
- package/dist/form/MediaPicker.d.ts +10 -0
- package/dist/form/MediaPicker.js +20 -0
- package/dist/form/SchemaForm.d.ts +21 -0
- package/dist/form/SchemaForm.js +67 -0
- package/dist/form/Trash.d.ts +1 -0
- package/dist/form/Trash.js +42 -0
- package/dist/form/fields/index.d.ts +1 -0
- package/dist/form/fields/index.js +110 -0
- package/dist/form/fields/objectList.d.ts +1 -0
- package/dist/form/fields/objectList.js +99 -0
- package/dist/form/index.d.ts +8 -0
- package/dist/form/index.js +8 -0
- package/dist/form/media/Lightbox.d.ts +4 -0
- package/dist/form/media/Lightbox.js +70 -0
- package/dist/form/media/MediaFolders.d.ts +20 -0
- package/dist/form/media/MediaFolders.js +116 -0
- package/dist/form/media/MediaGrid.d.ts +8 -0
- package/dist/form/media/MediaGrid.js +139 -0
- package/dist/form/media/UploadDialog.d.ts +4 -0
- package/dist/form/media/UploadDialog.js +37 -0
- package/dist/form/media/useMediaLibrary.d.ts +55 -0
- package/dist/form/media/useMediaLibrary.js +495 -0
- package/dist/form/prose/ProseBar.d.ts +21 -0
- package/dist/form/prose/ProseBar.js +111 -0
- package/dist/form/prose/ProseField.d.ts +11 -0
- package/dist/form/prose/ProseField.js +152 -0
- package/dist/form/prose/pages.d.ts +6 -0
- package/dist/form/prose/pages.js +13 -0
- package/dist/form/registry.d.ts +20 -0
- package/dist/form/registry.js +15 -0
- package/dist/frontmatter.d.ts +20 -0
- package/dist/frontmatter.js +202 -0
- package/dist/functions/_middleware.js +101 -0
- package/dist/functions/_shared/analytics-ga4.js +248 -0
- package/dist/functions/_shared/analytics-umami.js +119 -0
- package/dist/functions/_shared/assets.js +90 -0
- package/dist/functions/_shared/b2.js +300 -0
- package/dist/functions/_shared/delivery-crypto.js +99 -0
- package/dist/functions/_shared/delivery-purge.js +222 -0
- package/dist/functions/_shared/edge-cache.js +37 -0
- package/dist/functions/_shared/exif.js +53 -0
- package/dist/functions/_shared/github.js +138 -0
- package/dist/functions/_shared/imagesize.js +101 -0
- package/dist/functions/_shared/media-content.js +200 -0
- package/dist/functions/_shared/pending-media.js +98 -0
- package/dist/functions/_shared/refs.js +155 -0
- package/dist/functions/api/analytics.js +106 -0
- package/dist/functions/api/assets/delete.js +241 -0
- package/dist/functions/api/assets/folder.js +43 -0
- package/dist/functions/api/assets/folders.js +32 -0
- package/dist/functions/api/assets/move.js +253 -0
- package/dist/functions/api/assets/preview.js +17 -0
- package/dist/functions/api/assets/trash/delete.js +13 -0
- package/dist/functions/api/assets/trash/empty.js +16 -0
- package/dist/functions/api/assets/trash/restore.js +43 -0
- package/dist/functions/api/assets/trash.js +28 -0
- package/dist/functions/api/assets/upload.js +54 -0
- package/dist/functions/api/assets/usage.js +41 -0
- package/dist/functions/api/assets.js +94 -0
- package/dist/functions/api/deliveries.js +629 -0
- package/dist/functions/api/delivery/hero.js +21 -0
- package/dist/functions/api/delivery/image.js +35 -0
- package/dist/functions/api/delivery/multipart.js +261 -0
- package/dist/functions/api/pages/get.js +26 -0
- package/dist/functions/api/pages.js +354 -0
- package/dist/functions/api/publish.js +134 -0
- package/dist/functions/api/requests.js +57 -0
- package/dist/functions/api/settings.js +236 -0
- package/dist/functions/api/singleton.js +105 -0
- package/dist/functions/api/storage.js +125 -0
- package/dist/functions/index.js +124 -0
- package/dist/functions/site.js +38 -0
- package/dist/gallery.d.ts +28 -0
- package/dist/gallery.js +18 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/model.d.ts +2 -0
- package/dist/model.js +47 -0
- package/dist/normalize.d.ts +50 -0
- package/dist/normalize.js +120 -0
- package/dist/preview.d.ts +3 -0
- package/dist/preview.js +82 -0
- package/dist/prose.d.ts +17 -0
- package/dist/prose.js +205 -0
- package/dist/registries.d.ts +43 -0
- package/dist/registries.js +29 -0
- package/dist/serialize.d.ts +26 -0
- package/dist/serialize.js +136 -0
- package/dist/testing.d.ts +26 -0
- package/dist/testing.js +66 -0
- package/dist/theme.css +636 -0
- package/dist/yaml.d.ts +8 -0
- package/dist/yaml.js +90 -0
- 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;
|