@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,18 @@
|
|
|
1
|
+
import { type SiteAdminConfig } from "./site.js";
|
|
2
|
+
export type { SiteAdminConfig } from "./site.js";
|
|
3
|
+
export interface MountOptions {
|
|
4
|
+
/** Ciò che il kit non può sapere da solo: la forma dei contenuti di questo sito. */
|
|
5
|
+
site: SiteAdminConfig;
|
|
6
|
+
/** L'elemento in cui montare. Default: `#root`. */
|
|
7
|
+
root?: HTMLElement | null;
|
|
8
|
+
/**
|
|
9
|
+
* Namespace delle preferenze salvate nel browser (ordinamento dei media, ultima cartella
|
|
10
|
+
* aperta nel selettore).
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ Non si cambia a cuor leggero su un sito già in uso: sono le chiavi con cui quelle
|
|
13
|
+
* preferenze sono GIÀ state scritte nei browser di chi usa il pannello, e cambiarle le azzera
|
|
14
|
+
* in silenzio. Un sito che migra al kit deve passare qui il valore che usava prima.
|
|
15
|
+
*/
|
|
16
|
+
storagePrefix: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function mountAdmin({ site, root, storagePrefix }: MountOptions): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* L'ingresso `./app`: il pannello si monta con UNA chiamata.
|
|
3
|
+
*
|
|
4
|
+
* È opaco di proposito (DP-003). Il kit non esporta i pezzi della shell perché un sito possa
|
|
5
|
+
* assemblarsi la sua: comporre è esattamente ciò che ha prodotto la divergenza che questa
|
|
6
|
+
* estrazione ripara — il rail a gruppi e quello piatto nascono dallo stesso codice, e sono
|
|
7
|
+
* diversi solo perché ognuno poteva montarselo. Ciò che un sito cambia sono i suoi contenuti,
|
|
8
|
+
* i suoi colori e quali moduli sono accesi; non la forma del pannello.
|
|
9
|
+
*
|
|
10
|
+
* Ordine dei fogli di stile, e non è un dettaglio: il kit importa i PROPRI default
|
|
11
|
+
* (`theme.css`) e la propria impaginazione (`app.css`) da dentro `App.tsx`. Il sito importa il
|
|
12
|
+
* suo `theme.css` DOPO — cioè nel proprio `main.tsx`, dopo questo import — così i suoi valori
|
|
13
|
+
* vincono sui default senza che nessuno debba giocare con la specificità.
|
|
14
|
+
*/
|
|
15
|
+
import { StrictMode } from "react";
|
|
16
|
+
import { createRoot } from "react-dom/client";
|
|
17
|
+
import { setStoragePrefix } from "../index.js";
|
|
18
|
+
import { App } from "./App.js";
|
|
19
|
+
import { applyTheme, initialTheme } from "./lib/theme.js";
|
|
20
|
+
import { setSiteConfig } from "./site.js";
|
|
21
|
+
export function mountAdmin({ site, root, storagePrefix }) {
|
|
22
|
+
setSiteConfig(site);
|
|
23
|
+
setStoragePrefix(storagePrefix);
|
|
24
|
+
/* SINCRONO, prima del render: se il tema lo applicasse solo l'effetto dentro `App`, ci sarebbe
|
|
25
|
+
* un fotogramma con i valori chiari di default prima che React monti — visibile a chi
|
|
26
|
+
* preferisce il sistema scuro. `useTheme()` inizializza il suo stato con lo stesso
|
|
27
|
+
* `initialTheme()`, quindi il suo primo `applyTheme` è un no-op visivo, non un secondo lampo. */
|
|
28
|
+
applyTheme(initialTheme());
|
|
29
|
+
const el = root ?? document.getElementById("root");
|
|
30
|
+
if (!el)
|
|
31
|
+
throw new Error("adminkit: elemento di montaggio non trovato (atteso #root)");
|
|
32
|
+
createRoot(el).render(_jsx(StrictMode, { children: _jsx(App, {}) }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export type BookingStatus = "pending" | "confirming" | "confirmed" | "rejected" | "cancelled" | "completed" | "no_show" | "expired" | "anonymized";
|
|
2
|
+
export type Booking = {
|
|
3
|
+
id: string;
|
|
4
|
+
referenceCode: string;
|
|
5
|
+
appointmentTypeId: string | null;
|
|
6
|
+
typeName: string;
|
|
7
|
+
durationMinutes: number;
|
|
8
|
+
status: BookingStatus;
|
|
9
|
+
calendarSyncStatus: string;
|
|
10
|
+
visitorName: string | null;
|
|
11
|
+
visitorEmail: string | null;
|
|
12
|
+
visitorPhone: string | null;
|
|
13
|
+
visitorNotes: string | null;
|
|
14
|
+
adminNotes: string | null;
|
|
15
|
+
mode: "in_person" | "virtual";
|
|
16
|
+
location: string | null;
|
|
17
|
+
visitorTimezone: string;
|
|
18
|
+
startAt: string;
|
|
19
|
+
endAt: string;
|
|
20
|
+
googleEventUrl: string | null;
|
|
21
|
+
meetUrl: string | null;
|
|
22
|
+
source: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
};
|
|
25
|
+
export type AppointmentType = {
|
|
26
|
+
id: string;
|
|
27
|
+
slug: string;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
active: boolean;
|
|
31
|
+
durationMinutes: number | null;
|
|
32
|
+
bufferBeforeMinutes: number | null;
|
|
33
|
+
bufferAfterMinutes: number | null;
|
|
34
|
+
modePolicy: "in_person" | "virtual" | "both";
|
|
35
|
+
sortOrder: number;
|
|
36
|
+
adminColor: string;
|
|
37
|
+
};
|
|
38
|
+
export type BookingSettings = {
|
|
39
|
+
timezone: string;
|
|
40
|
+
address: string;
|
|
41
|
+
contactEmail: string;
|
|
42
|
+
contactPhone: string;
|
|
43
|
+
defaultDurationMinutes: number;
|
|
44
|
+
defaultBufferBeforeMinutes: number;
|
|
45
|
+
defaultBufferAfterMinutes: number;
|
|
46
|
+
slotStepMinutes: number;
|
|
47
|
+
minimumNoticeMinutes: number;
|
|
48
|
+
bookingHorizonDays: number;
|
|
49
|
+
dailyLimit: number;
|
|
50
|
+
weeklyLimit: number;
|
|
51
|
+
reminder1Minutes: number;
|
|
52
|
+
reminder2Minutes: number;
|
|
53
|
+
retentionDays: number;
|
|
54
|
+
};
|
|
55
|
+
export type AvailabilityWindow = {
|
|
56
|
+
weekday: number;
|
|
57
|
+
startMinute: number;
|
|
58
|
+
endMinute: number;
|
|
59
|
+
};
|
|
60
|
+
export type AvailabilityOverride = {
|
|
61
|
+
id?: string;
|
|
62
|
+
date: string;
|
|
63
|
+
kind: "closed" | "custom";
|
|
64
|
+
note?: string;
|
|
65
|
+
windows: Array<{
|
|
66
|
+
startMinute: number;
|
|
67
|
+
endMinute: number;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
export type GoogleCalendar = {
|
|
71
|
+
id: string;
|
|
72
|
+
summary: string;
|
|
73
|
+
primary: boolean;
|
|
74
|
+
accessRole: string;
|
|
75
|
+
busy: boolean;
|
|
76
|
+
booking: boolean;
|
|
77
|
+
supportsMeet: boolean;
|
|
78
|
+
};
|
|
79
|
+
export type BookingBlock = {
|
|
80
|
+
id: string;
|
|
81
|
+
start_at: string;
|
|
82
|
+
end_at: string;
|
|
83
|
+
note: string;
|
|
84
|
+
};
|
|
85
|
+
type BookingData = {
|
|
86
|
+
rows: Booking[] | null;
|
|
87
|
+
types: AppointmentType[];
|
|
88
|
+
settings: BookingSettings | null;
|
|
89
|
+
weekly: AvailabilityWindow[];
|
|
90
|
+
overrides: AvailabilityOverride[];
|
|
91
|
+
google: {
|
|
92
|
+
connected: boolean;
|
|
93
|
+
connection: {
|
|
94
|
+
account_email?: string;
|
|
95
|
+
token_status?: string;
|
|
96
|
+
} | null;
|
|
97
|
+
calendars: GoogleCalendar[];
|
|
98
|
+
};
|
|
99
|
+
activation: {
|
|
100
|
+
enabled: boolean;
|
|
101
|
+
ready: boolean;
|
|
102
|
+
checks: Record<string, boolean>;
|
|
103
|
+
} | null;
|
|
104
|
+
blocks: BookingBlock[];
|
|
105
|
+
pending: number;
|
|
106
|
+
upcoming: number;
|
|
107
|
+
loading: boolean;
|
|
108
|
+
busy: boolean;
|
|
109
|
+
error: string | null;
|
|
110
|
+
notice: string | null;
|
|
111
|
+
};
|
|
112
|
+
export declare function useBookings(): BookingData & {
|
|
113
|
+
load: () => Promise<void>;
|
|
114
|
+
action: (id: string, payload: Record<string, unknown>) => Promise<void>;
|
|
115
|
+
saveSettings: (value: BookingSettings) => Promise<void>;
|
|
116
|
+
saveType: (value: Partial<AppointmentType>, create?: boolean) => Promise<void>;
|
|
117
|
+
removeType: (id: string) => Promise<void>;
|
|
118
|
+
saveAvailability: (weekly: AvailabilityWindow[], overrides: AvailabilityOverride[]) => Promise<void>;
|
|
119
|
+
saveGoogle: (busy: string[], booking: string) => Promise<void>;
|
|
120
|
+
refreshGoogle: () => Promise<void>;
|
|
121
|
+
disconnectGoogle: () => Promise<void>;
|
|
122
|
+
setEnabled: (enabled: boolean) => Promise<void>;
|
|
123
|
+
addBlock: (value: {
|
|
124
|
+
startAt: string;
|
|
125
|
+
endAt: string;
|
|
126
|
+
note: string;
|
|
127
|
+
}) => Promise<void>;
|
|
128
|
+
removeBlock: (id: string) => Promise<void>;
|
|
129
|
+
createManual: (value: Record<string, unknown>) => Promise<string | null>;
|
|
130
|
+
clearNotice: () => void;
|
|
131
|
+
};
|
|
132
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
async function call(path, init) {
|
|
3
|
+
const response = await fetch(`/admin/api/booking${path}`, {
|
|
4
|
+
...init,
|
|
5
|
+
headers: { "Content-Type": "application/json", ...(init?.headers || {}) },
|
|
6
|
+
});
|
|
7
|
+
const data = await response.json().catch(() => ({}));
|
|
8
|
+
if (!response.ok)
|
|
9
|
+
throw new Error(data.error || `Errore ${response.status}`);
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
export function useBookings() {
|
|
13
|
+
const [state, setState] = useState({
|
|
14
|
+
rows: null, types: [], settings: null, weekly: [], overrides: [],
|
|
15
|
+
google: { connected: false, connection: null, calendars: [] },
|
|
16
|
+
activation: null, blocks: [], pending: 0, upcoming: 0,
|
|
17
|
+
loading: true, busy: false, error: null, notice: null,
|
|
18
|
+
});
|
|
19
|
+
const load = useCallback(async () => {
|
|
20
|
+
setState((value) => ({ ...value, loading: true, error: null }));
|
|
21
|
+
try {
|
|
22
|
+
const [list, types, settings, availability, google, activation, blocks] = await Promise.all([
|
|
23
|
+
call(""),
|
|
24
|
+
call("/types"),
|
|
25
|
+
call("/settings"),
|
|
26
|
+
call("/availability"),
|
|
27
|
+
call("/google"),
|
|
28
|
+
call("/activation"),
|
|
29
|
+
call("/blocks"),
|
|
30
|
+
]);
|
|
31
|
+
setState((value) => ({
|
|
32
|
+
...value, rows: list.bookings, pending: list.counts.pending, upcoming: list.counts.upcoming,
|
|
33
|
+
types: types.types, settings: settings.settings, weekly: availability.weekly,
|
|
34
|
+
overrides: availability.overrides, google, activation, blocks: blocks.blocks,
|
|
35
|
+
loading: false, error: null,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
setState((value) => ({ ...value, loading: false, error: error instanceof Error ? error.message : String(error) }));
|
|
40
|
+
}
|
|
41
|
+
}, []);
|
|
42
|
+
useEffect(() => { void load(); }, [load]);
|
|
43
|
+
const mutate = useCallback(async (task, notice) => {
|
|
44
|
+
setState((value) => ({ ...value, busy: true, error: null }));
|
|
45
|
+
try {
|
|
46
|
+
await task();
|
|
47
|
+
await load();
|
|
48
|
+
setState((value) => ({ ...value, busy: false, notice }));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
setState((value) => ({ ...value, busy: false, error: error instanceof Error ? error.message : String(error) }));
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}, [load]);
|
|
55
|
+
return {
|
|
56
|
+
...state,
|
|
57
|
+
load,
|
|
58
|
+
action: (id, payload) => mutate(() => call(`/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify(payload) }), "Prenotazione aggiornata."),
|
|
59
|
+
saveSettings: (value) => mutate(() => call("/settings", { method: "PUT", body: JSON.stringify(value) }), "Impostazioni salvate."),
|
|
60
|
+
saveType: (value, create = false) => mutate(() => call("/types", { method: create ? "POST" : "PUT", body: JSON.stringify(value) }), create ? "Tipologia creata." : "Tipologia aggiornata."),
|
|
61
|
+
removeType: (id) => mutate(() => call("/types", { method: "DELETE", body: JSON.stringify({ id }) }), "Tipologia archiviata."),
|
|
62
|
+
saveAvailability: (weekly, overrides) => mutate(() => call("/availability", { method: "PUT", body: JSON.stringify({ weekly, overrides }) }), "Disponibilità salvata."),
|
|
63
|
+
saveGoogle: (busy, booking) => mutate(() => call("/google", { method: "PUT", body: JSON.stringify({ busy, booking }) }), "Calendari aggiornati."),
|
|
64
|
+
refreshGoogle: () => mutate(() => call("/google", { method: "POST" }), "Elenco calendari aggiornato."),
|
|
65
|
+
disconnectGoogle: () => mutate(() => call("/google", { method: "DELETE" }), "Account Google scollegato."),
|
|
66
|
+
setEnabled: (enabled) => mutate(() => call("/activation", { method: "POST", body: JSON.stringify({ enabled }) }), enabled ? "Attivazione inviata: il sito sarà disponibile dopo il deploy." : "Disattivazione inviata."),
|
|
67
|
+
addBlock: (value) => mutate(() => call("/blocks", { method: "POST", body: JSON.stringify(value) }), "Blocco aggiunto."),
|
|
68
|
+
removeBlock: (id) => mutate(() => call("/blocks", { method: "DELETE", body: JSON.stringify({ id }) }), "Blocco rimosso."),
|
|
69
|
+
createManual: async (value) => {
|
|
70
|
+
let id = null;
|
|
71
|
+
await mutate(async () => {
|
|
72
|
+
const result = await call("", { method: "POST", body: JSON.stringify(value) });
|
|
73
|
+
id = result.booking.id;
|
|
74
|
+
}, "Appuntamento inserito; ora puoi approvarlo.");
|
|
75
|
+
return id;
|
|
76
|
+
},
|
|
77
|
+
clearNotice: () => setState((value) => ({ ...value, notice: null })),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type DeliveryStatus = "draft" | "uploading" | "active" | "archived";
|
|
2
|
+
export interface DeliverySummary {
|
|
3
|
+
id: string;
|
|
4
|
+
clientName: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
licenseNotes: string;
|
|
8
|
+
ctaEnabled: boolean;
|
|
9
|
+
ctaLabel: string;
|
|
10
|
+
ctaUrl: string;
|
|
11
|
+
expiresAt: string | null;
|
|
12
|
+
filesExpiresAt: string | null;
|
|
13
|
+
status: DeliveryStatus;
|
|
14
|
+
version: number;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
activatedAt: string | null;
|
|
18
|
+
packageCount: number;
|
|
19
|
+
linkCount: number;
|
|
20
|
+
storedBytes: number;
|
|
21
|
+
downloadCount: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DeliveryPackage {
|
|
24
|
+
id: string;
|
|
25
|
+
deliveryId: string;
|
|
26
|
+
label: string;
|
|
27
|
+
position: number;
|
|
28
|
+
galleryEnabled: boolean;
|
|
29
|
+
webEnabled: boolean;
|
|
30
|
+
state: "uploading" | "ready" | "failed";
|
|
31
|
+
sourceFingerprint: string;
|
|
32
|
+
fileCount: number;
|
|
33
|
+
sourceBytes: number;
|
|
34
|
+
storedBytes: number;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
images: DeliveryPackageImage[];
|
|
38
|
+
}
|
|
39
|
+
export interface DeliveryPackageImage {
|
|
40
|
+
index: number;
|
|
41
|
+
name: string;
|
|
42
|
+
previewUrl: string;
|
|
43
|
+
}
|
|
44
|
+
export type DeliveryHero = {
|
|
45
|
+
type: "package";
|
|
46
|
+
packageId: string;
|
|
47
|
+
index: number;
|
|
48
|
+
previewUrl: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: "object";
|
|
51
|
+
id: string;
|
|
52
|
+
sourceName: string;
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
previewUrl: string;
|
|
56
|
+
};
|
|
57
|
+
export interface DeliverySettings {
|
|
58
|
+
defaultHeroes: string[];
|
|
59
|
+
}
|
|
60
|
+
export interface DeliveryLink {
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
token: string;
|
|
64
|
+
pin: string | null;
|
|
65
|
+
url: string;
|
|
66
|
+
expiresAt: string | null;
|
|
67
|
+
revokedAt: string | null;
|
|
68
|
+
createdAt: string;
|
|
69
|
+
openedCount: number;
|
|
70
|
+
lastOpenedAt: string | null;
|
|
71
|
+
downloadCount: number;
|
|
72
|
+
lastDownloadAt: string | null;
|
|
73
|
+
}
|
|
74
|
+
export interface Delivery extends DeliverySummary {
|
|
75
|
+
packages: DeliveryPackage[];
|
|
76
|
+
links: DeliveryLink[];
|
|
77
|
+
heroes: DeliveryHero[];
|
|
78
|
+
}
|
|
79
|
+
export declare function useDeliveries(activeId?: string): {
|
|
80
|
+
rows: DeliverySummary[] | null;
|
|
81
|
+
detail: Delivery | null;
|
|
82
|
+
expiring: number;
|
|
83
|
+
settings: DeliverySettings;
|
|
84
|
+
error: string | null;
|
|
85
|
+
busy: boolean;
|
|
86
|
+
load: () => Promise<void>;
|
|
87
|
+
open: (id: string) => Promise<void>;
|
|
88
|
+
read: (id: string) => Promise<Delivery | null>;
|
|
89
|
+
action: <T>(payload: Record<string, unknown>, options?: {
|
|
90
|
+
reload?: boolean;
|
|
91
|
+
}) => Promise<T>;
|
|
92
|
+
refresh: () => Promise<void>;
|
|
93
|
+
setDetail: import("react").Dispatch<import("react").SetStateAction<Delivery | null>>;
|
|
94
|
+
};
|
|
95
|
+
export declare function deliveryAction<T = unknown>(payload: Record<string, unknown>): Promise<T>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { ApiError } from "../../index.js";
|
|
3
|
+
export function useDeliveries(activeId) {
|
|
4
|
+
const [rows, setRows] = useState(null);
|
|
5
|
+
const [detail, setDetail] = useState(null);
|
|
6
|
+
const [expiring, setExpiring] = useState(0);
|
|
7
|
+
const [settings, setSettings] = useState({ defaultHeroes: [] });
|
|
8
|
+
const [error, setError] = useState(null);
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
const load = useCallback(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const out = await getJson("/admin/api/deliveries");
|
|
13
|
+
setRows(out.deliveries);
|
|
14
|
+
setExpiring(out.expiring);
|
|
15
|
+
setSettings(out.settings || { defaultHeroes: [] });
|
|
16
|
+
setError(null);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
setError(errorMessage(e));
|
|
20
|
+
}
|
|
21
|
+
}, []);
|
|
22
|
+
/* Rileggere UN transfer senza toccare `detail`: serve al transfer appena nato dentro
|
|
23
|
+
l'editor, che non ha una rotta aperta e non può farsi rimontare — sotto ha un upload che
|
|
24
|
+
cammina, e un rimontaggio lo interromperebbe. */
|
|
25
|
+
const read = useCallback(async (id) => {
|
|
26
|
+
try {
|
|
27
|
+
const out = await getJson(`/admin/api/deliveries?id=${encodeURIComponent(id)}`);
|
|
28
|
+
return out.delivery;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}, []);
|
|
34
|
+
const open = useCallback(async (id) => {
|
|
35
|
+
setBusy(true);
|
|
36
|
+
try {
|
|
37
|
+
const out = await getJson(`/admin/api/deliveries?id=${encodeURIComponent(id)}`);
|
|
38
|
+
setDetail(out.delivery);
|
|
39
|
+
setError(null);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
setError(errorMessage(e));
|
|
43
|
+
setDetail(null);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setBusy(false);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
useEffect(() => { void load(); }, [load]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (activeId)
|
|
52
|
+
void open(activeId);
|
|
53
|
+
else
|
|
54
|
+
setDetail(null);
|
|
55
|
+
}, [activeId, open]);
|
|
56
|
+
const action = useCallback(async (payload, options = {}) => {
|
|
57
|
+
setBusy(true);
|
|
58
|
+
try {
|
|
59
|
+
const out = await postJson("/admin/api/deliveries", payload);
|
|
60
|
+
setError(null);
|
|
61
|
+
if (options.reload !== false)
|
|
62
|
+
await load();
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (options.reload !== false)
|
|
67
|
+
setError(errorMessage(e));
|
|
68
|
+
throw e;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setBusy(false);
|
|
72
|
+
}
|
|
73
|
+
}, [load]);
|
|
74
|
+
const refresh = useCallback(async () => {
|
|
75
|
+
await load();
|
|
76
|
+
if (activeId)
|
|
77
|
+
await open(activeId);
|
|
78
|
+
}, [activeId, load, open]);
|
|
79
|
+
return { rows, detail, expiring, settings, error, busy, load, open, read, action, refresh, setDetail };
|
|
80
|
+
}
|
|
81
|
+
/* Un'azione sui transfer senza passare dall'hook: la usa la coda dei caricamenti, che vive
|
|
82
|
+
fuori da React e deve poter chiudere un pacchetto anche mentre il pannello è altrove. */
|
|
83
|
+
export async function deliveryAction(payload) {
|
|
84
|
+
return postJson("/admin/api/deliveries", payload);
|
|
85
|
+
}
|
|
86
|
+
async function getJson(url) {
|
|
87
|
+
return request(url);
|
|
88
|
+
}
|
|
89
|
+
async function postJson(url, body) {
|
|
90
|
+
return request(url, {
|
|
91
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function request(url, init) {
|
|
95
|
+
const res = await fetch(url, { cache: "no-store", ...init });
|
|
96
|
+
const body = await res.json().catch(() => ({}));
|
|
97
|
+
if (!res.ok || body.ok === false) {
|
|
98
|
+
throw new ApiError(body.error || `HTTP ${res.status}`, body.code || "error", res.status);
|
|
99
|
+
}
|
|
100
|
+
return body;
|
|
101
|
+
}
|
|
102
|
+
function errorMessage(e) {
|
|
103
|
+
return e instanceof Error ? e.message : String(e);
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function filesFromDrop(data: DataTransfer): Promise<File[]>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { deliveryRelativeName, setDeliveryRelativePath } from "./delivery-upload";
|
|
2
|
+
/* Mantiene le cartelle anche quando arrivano da drag & drop. Le API dei browser
|
|
3
|
+
* hanno due forme (File System Access e webkitEntry), quindi usiamo entrambe e
|
|
4
|
+
* ripieghiamo sulla FileList senza perdere i normali file trascinati. */
|
|
5
|
+
export async function filesFromDrop(data) {
|
|
6
|
+
const collected = [];
|
|
7
|
+
/* getAsFileSystemHandle va invocata mentre il gesto drop è ancora attivo.
|
|
8
|
+
Acquisiamo quindi tutte le promise prima di attendere la prima cartella. */
|
|
9
|
+
const descriptors = [...data.items].filter((item) => item.kind === "file").map((item) => {
|
|
10
|
+
const modern = item.getAsFileSystemHandle;
|
|
11
|
+
const legacy = item.webkitGetAsEntry?.();
|
|
12
|
+
return { item, handle: modern ? modern.call(item) : null, legacy };
|
|
13
|
+
});
|
|
14
|
+
for (const descriptor of descriptors) {
|
|
15
|
+
const { item } = descriptor;
|
|
16
|
+
if (item.kind !== "file")
|
|
17
|
+
continue;
|
|
18
|
+
if (descriptor.handle) {
|
|
19
|
+
const handle = await descriptor.handle.catch(() => null);
|
|
20
|
+
if (handle) {
|
|
21
|
+
await walkHandle(handle, "", collected);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (descriptor.legacy) {
|
|
26
|
+
await walkLegacy(descriptor.legacy, "", collected);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const file = item.getAsFile();
|
|
30
|
+
if (file)
|
|
31
|
+
collected.push(file);
|
|
32
|
+
}
|
|
33
|
+
if (!collected.length)
|
|
34
|
+
collected.push(...data.files);
|
|
35
|
+
const unique = new Map();
|
|
36
|
+
for (const file of collected) {
|
|
37
|
+
const path = deliveryRelativeName(file);
|
|
38
|
+
unique.set(`${path}\0${file.size}\0${file.lastModified}`, file);
|
|
39
|
+
}
|
|
40
|
+
return [...unique.values()];
|
|
41
|
+
}
|
|
42
|
+
async function walkHandle(handle, parent, out) {
|
|
43
|
+
const path = parent ? `${parent}/${handle.name}` : handle.name;
|
|
44
|
+
if (handle.kind === "file") {
|
|
45
|
+
const file = await handle.getFile();
|
|
46
|
+
setDeliveryRelativePath(file, path);
|
|
47
|
+
out.push(file);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const directory = handle;
|
|
51
|
+
for await (const child of directory.values())
|
|
52
|
+
await walkHandle(child, path, out);
|
|
53
|
+
}
|
|
54
|
+
async function walkLegacy(entry, parent, out) {
|
|
55
|
+
const path = parent ? `${parent}/${entry.name}` : entry.name;
|
|
56
|
+
if (entry.isFile) {
|
|
57
|
+
const file = await new Promise((resolve, reject) => entry.file(resolve, reject));
|
|
58
|
+
setDeliveryRelativePath(file, path);
|
|
59
|
+
out.push(file);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!entry.isDirectory)
|
|
63
|
+
return;
|
|
64
|
+
const reader = entry.createReader();
|
|
65
|
+
for (;;) {
|
|
66
|
+
const page = await new Promise((resolve, reject) => reader.readEntries(resolve, reject));
|
|
67
|
+
if (!page.length)
|
|
68
|
+
break;
|
|
69
|
+
for (const child of page)
|
|
70
|
+
await walkLegacy(child, path, out);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export interface DeliveryUploadProgress {
|
|
2
|
+
phase: "originals" | "derivatives" | "finalizing";
|
|
3
|
+
file: string;
|
|
4
|
+
doneFiles: number;
|
|
5
|
+
totalFiles: number;
|
|
6
|
+
sourceBytes: number;
|
|
7
|
+
uploadedBytes: number;
|
|
8
|
+
transferredBytes: number;
|
|
9
|
+
startedAt: number;
|
|
10
|
+
transferStartedAt?: number;
|
|
11
|
+
note?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DeliveryManifestEntry {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
original: ZipMember;
|
|
17
|
+
web?: ZipMember;
|
|
18
|
+
thumb?: ZipMember;
|
|
19
|
+
width?: number;
|
|
20
|
+
height?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface DeliveryManifest {
|
|
23
|
+
version: 1;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
entries: DeliveryManifestEntry[];
|
|
26
|
+
skipped: {
|
|
27
|
+
name: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
interface ZipMember {
|
|
32
|
+
offset: number;
|
|
33
|
+
length: number;
|
|
34
|
+
crc32: number;
|
|
35
|
+
}
|
|
36
|
+
interface UploadOptions {
|
|
37
|
+
galleryEnabled: boolean;
|
|
38
|
+
webEnabled: boolean;
|
|
39
|
+
}
|
|
40
|
+
interface UploadResult {
|
|
41
|
+
manifest: DeliveryManifest;
|
|
42
|
+
fileCount: number;
|
|
43
|
+
sourceBytes: number;
|
|
44
|
+
storedBytes: number;
|
|
45
|
+
}
|
|
46
|
+
export interface UploadControl {
|
|
47
|
+
gate(): Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
export declare class UploadCancelled extends Error {
|
|
50
|
+
constructor();
|
|
51
|
+
}
|
|
52
|
+
export declare function uploadDeliveryPackage(deliveryId: string, packageId: string, selected: File[], options: UploadOptions, onProgress: (progress: DeliveryUploadProgress) => void, control?: UploadControl): Promise<UploadResult>;
|
|
53
|
+
interface ZipSink {
|
|
54
|
+
write(data: Uint8Array): Promise<void>;
|
|
55
|
+
close(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export declare class ZipStream {
|
|
58
|
+
private sink;
|
|
59
|
+
private offset;
|
|
60
|
+
private entries;
|
|
61
|
+
constructor(sink: ZipSink);
|
|
62
|
+
add(nameValue: string, blob: Blob, progress?: (read: number) => void, modified?: Date): Promise<ZipMember>;
|
|
63
|
+
close(): Promise<void>;
|
|
64
|
+
private push;
|
|
65
|
+
}
|
|
66
|
+
export declare function setDeliveryRelativePath(file: File, path: string): void;
|
|
67
|
+
export declare function deliveryRelativeName(file: File): string;
|
|
68
|
+
export declare function deliverySelectionName(files: File[]): string;
|
|
69
|
+
export declare function uploadDeliveryHeroObject(deliveryId: string, packageId: string, file: File): Promise<{
|
|
70
|
+
type: "object";
|
|
71
|
+
id: string;
|
|
72
|
+
sourceName: string;
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
bytes: number;
|
|
76
|
+
}>;
|
|
77
|
+
export declare function deliverySourceFingerprint(files: File[]): Promise<string>;
|
|
78
|
+
export {};
|