@cardinali-studio/adminkit 2.2.20 → 2.2.22
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 +38 -2
- package/dist/api.js +13 -0
- package/dist/app/App.d.ts +1 -0
- package/dist/app/App.js +36 -15
- package/dist/app/NextAdminApp.d.ts +8 -0
- package/dist/app/NextAdminApp.js +3 -0
- package/dist/app/lib/admin-interface.d.ts +11 -0
- package/dist/app/lib/admin-interface.js +45 -0
- package/dist/app/next.css +826 -0
- package/dist/app/routes/AdminInterfaceSettings.d.ts +12 -0
- package/dist/app/routes/AdminInterfaceSettings.js +14 -0
- package/dist/app/routes/Deliveries.js +1 -1
- package/dist/app/routes/GallerySectionsEditor.d.ts +1 -1
- package/dist/app/routes/GallerySectionsEditor.js +14 -2
- package/dist/app/routes/HomeEditor.js +2 -2
- package/dist/app/routes/Overview.js +4 -4
- package/dist/app/routes/PageEditor.js +12 -5
- package/dist/app/routes/PortfolioWorksEditor.d.ts +1 -1
- package/dist/app/routes/PortfolioWorksEditor.js +11 -3
- package/dist/app/routes/Requests.js +2 -2
- package/dist/app/routes/SiteSettingsEditor.d.ts +4 -1
- package/dist/app/routes/SiteSettingsEditor.js +3 -2
- package/dist/app/ui/MediaSlot.js +1 -1
- package/dist/app/ui/OpenCard.js +22 -1
- package/dist/app/ui/icons.js +5 -2
- package/dist/form/fields/index.js +2 -1
- package/dist/form/media/Lightbox.js +33 -3
- package/dist/form/media/MediaGrid.js +9 -6
- package/dist/form/media/media-model.d.ts +2 -0
- package/dist/form/media/media-model.js +8 -0
- package/dist/form/media/useMediaLibrary.d.ts +11 -2
- package/dist/form/media/useMediaLibrary.js +89 -24
- package/dist/functions/_shared/d1-media.js +105 -91
- package/dist/functions/_shared/media-alt.js +61 -0
- package/dist/functions/api/assets/delete.js +20 -3
- package/dist/functions/api/assets/media.js +57 -2
- package/dist/functions/api/assets/move.js +5 -1
- package/dist/functions/api/assets/upload.js +3 -1
- package/dist/functions/api/assets.js +5 -2
- package/dist/gallery.d.ts +2 -0
- package/dist/media.d.ts +19 -0
- package/dist/media.js +132 -0
- package/dist/normalize.d.ts +1 -0
- package/dist/normalize.js +2 -1
- package/dist/persistence.d.ts +8 -0
- package/dist/persistence.js +78 -6
- package/dist/theme.css +42 -0
- package/package.json +2 -2
package/dist/api.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export declare class ApiError extends Error {
|
|
|
6
6
|
details: unknown;
|
|
7
7
|
constructor(message: string, code: string, status: number, details?: unknown);
|
|
8
8
|
}
|
|
9
|
+
export interface AssetAltText {
|
|
10
|
+
/** Vero significa che il sito deve emettere deliberatamente alt="". */
|
|
11
|
+
decorative: boolean;
|
|
12
|
+
/** Chiave "default" nei siti monolingui, codice lingua nei siti multilingui. */
|
|
13
|
+
texts: Record<string, string>;
|
|
14
|
+
}
|
|
9
15
|
export interface AssetFile {
|
|
10
16
|
key: string;
|
|
11
17
|
name: string;
|
|
@@ -14,9 +20,13 @@ export interface AssetFile {
|
|
|
14
20
|
uploaded?: string;
|
|
15
21
|
capturedAt?: string;
|
|
16
22
|
type?: string;
|
|
23
|
+
alt?: AssetAltText;
|
|
17
24
|
w?: number | null;
|
|
18
25
|
h?: number | null;
|
|
19
26
|
}
|
|
27
|
+
/** Risolve il testo Media da copiare nel singolo utilizzo editoriale. Il campo della pagina
|
|
28
|
+
* resta un override: cambiare in seguito il default del file non riscrive contenuti esistenti. */
|
|
29
|
+
export declare function assetAltText(file: Pick<AssetFile, "alt">, locale?: string): string;
|
|
20
30
|
export interface AssetFolder {
|
|
21
31
|
prefix: string;
|
|
22
32
|
name: string;
|
|
@@ -57,11 +67,15 @@ export interface RepoPage {
|
|
|
57
67
|
sha?: string;
|
|
58
68
|
}>;
|
|
59
69
|
}
|
|
60
|
-
export type
|
|
70
|
+
export type ContentDocumentPublishTarget = {
|
|
61
71
|
documentId: string;
|
|
62
72
|
expectedDraftRevisionId: string | null;
|
|
63
73
|
includeSiteAccess?: boolean;
|
|
64
74
|
};
|
|
75
|
+
export type ContentMediaPublishTarget = {
|
|
76
|
+
mediaOperationId: string;
|
|
77
|
+
};
|
|
78
|
+
export type ContentPublishTarget = ContentDocumentPublishTarget | ContentMediaPublishTarget;
|
|
65
79
|
export type ContentPublishDocument = {
|
|
66
80
|
documentId: string;
|
|
67
81
|
kind: string;
|
|
@@ -203,6 +217,17 @@ export declare const api: {
|
|
|
203
217
|
dependentDocuments?: number;
|
|
204
218
|
remainingAhead?: number;
|
|
205
219
|
}>;
|
|
220
|
+
contentPublishMedia: (mediaOperationId: string) => Promise<{
|
|
221
|
+
ok: true;
|
|
222
|
+
published: boolean;
|
|
223
|
+
releaseId?: string;
|
|
224
|
+
attemptId?: string;
|
|
225
|
+
message?: string;
|
|
226
|
+
verified?: boolean;
|
|
227
|
+
observedReleaseId?: string | null;
|
|
228
|
+
documents?: number;
|
|
229
|
+
remainingAhead?: number;
|
|
230
|
+
}>;
|
|
206
231
|
publishState: () => Promise<{
|
|
207
232
|
ok: true;
|
|
208
233
|
configured: boolean;
|
|
@@ -257,6 +282,9 @@ export declare const api: {
|
|
|
257
282
|
error: string;
|
|
258
283
|
}[];
|
|
259
284
|
warning?: string;
|
|
285
|
+
operationId?: string;
|
|
286
|
+
requiresPublication?: boolean;
|
|
287
|
+
completed?: number;
|
|
260
288
|
}>;
|
|
261
289
|
/** Riscrive i riferimenti, poi elimina gli originali le cui copie sono state verificate. */
|
|
262
290
|
moveApplyRefs: (applyRefs: Record<string, string>) => Promise<{
|
|
@@ -274,7 +302,12 @@ export declare const api: {
|
|
|
274
302
|
ok: true;
|
|
275
303
|
operations: MediaOperationStatus[];
|
|
276
304
|
}>;
|
|
277
|
-
|
|
305
|
+
saveMediaAlt: (key: string, alt: AssetAltText) => Promise<{
|
|
306
|
+
ok: true;
|
|
307
|
+
key: string;
|
|
308
|
+
alt: AssetAltText;
|
|
309
|
+
}>;
|
|
310
|
+
resumeMediaOperation: (operationId: string, releaseId: string | null) => Promise<{
|
|
278
311
|
ok: true;
|
|
279
312
|
completed: number;
|
|
280
313
|
failed: number;
|
|
@@ -313,6 +346,9 @@ export declare const api: {
|
|
|
313
346
|
d1?: boolean;
|
|
314
347
|
deferred?: boolean;
|
|
315
348
|
operationId?: string;
|
|
349
|
+
requiresPublication?: boolean;
|
|
350
|
+
completed?: number;
|
|
351
|
+
failed?: number;
|
|
316
352
|
}>;
|
|
317
353
|
requests: () => Promise<{
|
|
318
354
|
ok: true;
|
package/dist/api.js
CHANGED
|
@@ -34,6 +34,13 @@ const post = (url, data) => call(url, {
|
|
|
34
34
|
headers: { "Content-Type": "application/json" },
|
|
35
35
|
body: JSON.stringify(data),
|
|
36
36
|
});
|
|
37
|
+
/** Risolve il testo Media da copiare nel singolo utilizzo editoriale. Il campo della pagina
|
|
38
|
+
* resta un override: cambiare in seguito il default del file non riscrive contenuti esistenti. */
|
|
39
|
+
export function assetAltText(file, locale = "default") {
|
|
40
|
+
if (file.alt?.decorative)
|
|
41
|
+
return "";
|
|
42
|
+
return file.alt?.texts[locale] ?? file.alt?.texts.default ?? "";
|
|
43
|
+
}
|
|
37
44
|
/** Builds the only D1 Save wire shape. The Worker recomputes these hashes too,
|
|
38
45
|
* so retries are deterministic rather than client-trusted. */
|
|
39
46
|
export async function contentSaveRequest(command) {
|
|
@@ -67,6 +74,7 @@ export const api = {
|
|
|
67
74
|
* materialize, while the panel only displays the pending immutable inputs. */
|
|
68
75
|
contentPublishState: () => call("/admin/api/content/publish"),
|
|
69
76
|
contentPublish: (target) => post("/admin/api/content/publish", target),
|
|
77
|
+
contentPublishMedia: (mediaOperationId) => post("/admin/api/content/publish", { mediaOperationId }),
|
|
70
78
|
// ---- pubblicazione
|
|
71
79
|
/* Salvare scrive sulla bozza e non fa partire nessuna build; è questo che manda online. Due
|
|
72
80
|
* gesti separati, quindi due chiamate: una chiede quanto c'è di non pubblicato, l'altra lo
|
|
@@ -99,6 +107,11 @@ export const api = {
|
|
|
99
107
|
/** Riscrive i riferimenti, poi elimina gli originali le cui copie sono state verificate. */
|
|
100
108
|
moveApplyRefs: (applyRefs) => post("/admin/api/assets/move", { applyRefs }),
|
|
101
109
|
mediaOperations: () => call("/admin/api/assets/media"),
|
|
110
|
+
saveMediaAlt: (key, alt) => call("/admin/api/assets/media", {
|
|
111
|
+
method: "PATCH",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({ key, alt }),
|
|
114
|
+
}),
|
|
102
115
|
resumeMediaOperation: (operationId, releaseId) => post("/admin/api/assets/media", { action: "resume", operationId, releaseId }),
|
|
103
116
|
cancelMediaOperation: (operationId) => post("/admin/api/assets/media", { action: "cancel", operationId }),
|
|
104
117
|
usage: (target) => post("/admin/api/assets/usage", target),
|
package/dist/app/App.d.ts
CHANGED
package/dist/app/App.js
CHANGED
|
@@ -19,6 +19,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
19
19
|
* i default del kit, poi l'identità di questo sito che li sovrascrive, poi l'impaginazione. */
|
|
20
20
|
import "../theme.css";
|
|
21
21
|
import "./app.css";
|
|
22
|
+
import "./next.css";
|
|
22
23
|
import { MediaFolders, MediaGrid, ProsePages, Trash, useMediaLibrary, usePicker } from "../form/index.js";
|
|
23
24
|
import { Preview, isLocalPreview } from "../preview.js";
|
|
24
25
|
import { contentPersistenceMode, site } from "./site.js";
|
|
@@ -31,7 +32,8 @@ import { Overview } from "./routes/Overview";
|
|
|
31
32
|
import { PagesList } from "./routes/PagesList";
|
|
32
33
|
import { PageEditor, PageHeader } from "./routes/PageEditor";
|
|
33
34
|
import { PageSeoEditor } from "./routes/PageSeoEditor";
|
|
34
|
-
import { SiteSettingsEditor, WebsiteSeoSettingsEditor } from "./routes/SiteSettingsEditor";
|
|
35
|
+
import { SiteSettingsEditor, WebsiteSeoSettingsEditor, } from "./routes/SiteSettingsEditor";
|
|
36
|
+
import { SiteSettingsUnavailable } from "./routes/AdminInterfaceSettings";
|
|
35
37
|
import { MediaSettingsEditor } from "./routes/MediaSettingsEditor";
|
|
36
38
|
import { HomeEditor } from "./routes/HomeEditor";
|
|
37
39
|
import { RequestsList, RequestDetail } from "./routes/Requests";
|
|
@@ -46,7 +48,16 @@ import { NewPageButton } from "./routes/PageActions";
|
|
|
46
48
|
import { BookingAgenda, BookingDetail, BookingsList, BookingSettingsHub, } from "./routes/Bookings";
|
|
47
49
|
import { Icon } from "./ui/icons";
|
|
48
50
|
import { useTheme } from "./lib/theme";
|
|
51
|
+
import { useAdminInterface } from "./lib/admin-interface";
|
|
52
|
+
import { NextAdminApp } from "./NextAdminApp";
|
|
49
53
|
export function App() {
|
|
54
|
+
const { adminInterface, setAdminInterface } = useAdminInterface();
|
|
55
|
+
const current = (_jsx(CurrentAdminApp, { adminInterface: adminInterface, setAdminInterface: setAdminInterface }));
|
|
56
|
+
return adminInterface === "next"
|
|
57
|
+
? _jsx(NextAdminApp, { current: current, adminInterface: adminInterface, setAdminInterface: setAdminInterface })
|
|
58
|
+
: current;
|
|
59
|
+
}
|
|
60
|
+
function CurrentAdminApp({ adminInterface, setAdminInterface, }) {
|
|
50
61
|
const [route, go] = useHashRoute();
|
|
51
62
|
const { theme, toggle: toggleTheme } = useTheme();
|
|
52
63
|
/* Si entra dalla Panoramica, non dalle Pagine: aprendo il pannello la domanda è quasi sempre
|
|
@@ -176,7 +187,7 @@ export function App() {
|
|
|
176
187
|
* Il badge invece sparisce: un contatore su una funzione spenta conterebbe un elenco che il
|
|
177
188
|
* server non serve nemmeno (le sue rotte rispondono 404). */
|
|
178
189
|
{ title: "Attività", items: [
|
|
179
|
-
{ id: "deliveries", label: "
|
|
190
|
+
{ id: "deliveries", label: "Transfer", icon: Icon.delivery, badge: moduleOn("delivery") ? deliveries.expiring : 0 },
|
|
180
191
|
{ id: "bookings", label: "Prenotazioni", icon: Icon.calendar, badge: moduleOn("booking") ? bookings.pending : 0 },
|
|
181
192
|
{ id: "requests", label: "Richieste", icon: Icon.mail, badge: moduleOn("requests") ? requests.pending : 0 },
|
|
182
193
|
...(local ? [{ id: "preview", label: "Anteprima", icon: Icon.play }] : []),
|
|
@@ -221,7 +232,7 @@ export function App() {
|
|
|
221
232
|
/* Consegne non ripete il titolo qui: il nome del transfer sta nella sua schermata, dove
|
|
222
233
|
si modifica, e si aggiorna mentre lo si scrive. Averlo in due posti significa averne
|
|
223
234
|
uno dei due sempre vecchio. */
|
|
224
|
-
: section === "deliveries" ? (deliverySettingsOpen ? "Impostazioni" : "
|
|
235
|
+
: section === "deliveries" ? (deliverySettingsOpen ? "Impostazioni" : "Transfer")
|
|
225
236
|
: section === "bookings" ? (bookingSettingsOpen ? "Impostazioni prenotazioni" : openBooking ? openBooking.visitorName || openBooking.referenceCode : "Prenotazioni")
|
|
226
237
|
: section === "requests" ? (openReq ? openReq.name : "Richieste")
|
|
227
238
|
: section === "settings" ? (settingsView === "seo" ? "SEO" : "Sito")
|
|
@@ -237,7 +248,17 @@ export function App() {
|
|
|
237
248
|
const p = all.slice(0, i + 1).join("/") + "/";
|
|
238
249
|
return { prefix: p, label: media.labelOf(p) };
|
|
239
250
|
});
|
|
240
|
-
|
|
251
|
+
/* La testata non ripete il contesto già interamente leggibile nella colonna sinistra.
|
|
252
|
+
* Resta invece quando porta informazione ulteriore: controlli della pagina, antenati di una
|
|
253
|
+
* cartella, oppure il nome di un elemento che nell'elenco può essere troncato. */
|
|
254
|
+
const redundantPaneContext = (section === "pages" && !editing)
|
|
255
|
+
|| section === "home"
|
|
256
|
+
|| section === "deliveries"
|
|
257
|
+
|| section === "settings"
|
|
258
|
+
|| (section === "requests" && !openReq)
|
|
259
|
+
|| (section === "bookings" && !openBooking)
|
|
260
|
+
|| (section === "media" && (view === "trash" || mediaSettingsOpen || mediaCrumbs.length === 0));
|
|
261
|
+
return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), "aria-label": n.label, disabled: section === "media" && media.mutating, onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", "aria-label": "Vedi il sito", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, "aria-label": theme === "dark" ? "Tema chiaro" : "Tema scuro", children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), "aria-label": "Impostazioni", disabled: section === "media" && media.mutating, onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Pagine" }), list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) })] }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
|
|
241
262
|
: !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
242
263
|
: _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
|
|
243
264
|
if (editing?.slug === from)
|
|
@@ -253,16 +274,16 @@ export function App() {
|
|
|
253
274
|
else
|
|
254
275
|
next.add(id);
|
|
255
276
|
return next;
|
|
256
|
-
}) }) })] })), section === "deliveries" && (_jsxs(_Fragment, { children: [
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
277
|
+
}) }) })] })), section === "deliveries" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "col-head col-head-actions", children: [_jsx("span", { className: "col-title", children: "Transfer" }), _jsx(NewDeliveryButton, { onCreate: () => {
|
|
278
|
+
setQuickSelectedId(undefined);
|
|
279
|
+
setQuickKey((value) => value + 1);
|
|
280
|
+
go("/deliveries");
|
|
281
|
+
} })] }), _jsx("div", { className: "col-body", children: deliveries.error ? _jsx("p", { className: "ak-error", children: deliveries.error })
|
|
261
282
|
: !deliveries.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
262
283
|
: deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
263
284
|
: _jsx(DeliveriesList, { rows: deliveries.rows, current: activeDeliveryId || quickSelectedId, onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); } }) }), _jsxs("div", { className: "dl-col-foot", children: [deliveries.rows && _jsx(DeliveriesTotal, { rows: deliveries.rows }), _jsxs("button", { className: "col-item" + (deliverySettingsOpen ? " is-on" : ""), onClick: () => { setQuickSelectedId(undefined); go("/deliveries/settings"); }, children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] })), section === "bookings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: bookings.rows ? `${bookings.rows.length} appuntamenti` : "Prenotazioni" }) }), _jsx("div", { className: "col-body", children: bookings.error ? _jsx("p", { className: "ak-error", children: bookings.error })
|
|
264
285
|
: !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
265
|
-
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
286
|
+
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" && !mediaSettingsOpen && mediaCrumbs.length > 0 ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), disabled: media.mutating, onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), disabled: media.mutating, onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : !redundantPaneContext ? (_jsx("h1", { className: "pane-title", children: heading })) : null, view === "media" && !mediaSettingsOpen && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && !mediaSettingsOpen && (_jsx("button", { className: "ak-btn ak-btn-primary", disabled: media.busy || media.mutating, onClick: () => media.openUpload(), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
266
287
|
/* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
|
|
267
288
|
rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
|
|
268
289
|
il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
|
|
@@ -283,9 +304,9 @@ export function App() {
|
|
|
283
304
|
se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
|
|
284
305
|
home.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: home.error }), _jsx("button", { className: "ak-btn", onClick: () => void home.load(), children: "Riprova" })] }))
|
|
285
306
|
: home.data ? _jsx(HomeEditorView, { data: home.data, group: homeGroup, onChange: home.change, pickMedia: pickMedia })
|
|
286
|
-
: _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] })) : settings.data ? (settingsView === "seo"
|
|
307
|
+
: _jsx("p", { className: "ak-hint", children: "Carico\u2026" })), section === "settings" && (settings.error ? (settingsView === "site" ? (_jsx(SiteSettingsUnavailable, { error: settings.error, onRetry: () => void settings.load(), adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface })) : (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: settings.error }), _jsx("button", { className: "ak-btn", onClick: () => void settings.load(), children: "Riprova" })] }))) : settings.data ? (settingsView === "seo"
|
|
287
308
|
? _jsx(WebsiteSeoSettingsEditor, { value: settings.data, onChange: settings.change })
|
|
288
|
-
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (mediaSettingsOpen
|
|
309
|
+
: _jsx(SiteSettingsEditor, { value: settings.data, onChange: settings.change, pickMediaFiles: pickMediaFiles, adminInterface: adminInterface, onAdminInterfaceChange: setAdminInterface, focus: route[1], passwordDraftPending: settings.accessDraftPending, onDiscardPasswordDraft: () => void settings.discardSiteAccess().then(() => publish.reload()) })) : _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })), view === "media" && (mediaSettingsOpen
|
|
289
310
|
? _jsx(MediaSettingsEditor, { lib: media })
|
|
290
311
|
: _jsx(MediaGrid, { lib: media })), view === "trash" && _jsx(Trash, {}), section === "requests" && (requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
291
312
|
: openReq ? (_jsx(RequestDetail, { row: openReq, onMark: (h) => void requests.mark([openReq.id], h), onRemove: () => { void requests.remove([openReq.id]); go("/requests"); } }))
|
|
@@ -317,10 +338,10 @@ function hasList(section) {
|
|
|
317
338
|
const OPTIONAL_SECTIONS = {
|
|
318
339
|
deliveries: {
|
|
319
340
|
module: "delivery",
|
|
320
|
-
title: "
|
|
341
|
+
title: "Transfer",
|
|
321
342
|
lead: "Manda le fotografie a un cliente con un link privato, senza allegati e senza servizi di terze parti.",
|
|
322
343
|
points: [
|
|
323
|
-
"Un indirizzo riservato per ogni
|
|
344
|
+
"Un indirizzo riservato per ogni Transfer, che scade da solo",
|
|
324
345
|
"Il cliente scarica tutto in una volta, o solo le foto che gli servono",
|
|
325
346
|
"Vedi quante volte è stato scaricato, e da quando",
|
|
326
347
|
],
|
|
@@ -341,7 +362,7 @@ const OPTIONAL_SECTIONS = {
|
|
|
341
362
|
lead: "Le richieste che arrivano dal modulo di contatto del sito, raccolte qui invece che in una casella di posta.",
|
|
342
363
|
points: [
|
|
343
364
|
"Nessuna richiesta persa fra le altre email",
|
|
344
|
-
"Segni quali hai già
|
|
365
|
+
"Segni quali hai già letto",
|
|
345
366
|
"Rispondi con un click, dall'indirizzo di sempre",
|
|
346
367
|
],
|
|
347
368
|
},
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Dispatch, ReactNode, SetStateAction } from "react";
|
|
2
|
+
import type { AdminInterface } from "./lib/admin-interface.js";
|
|
3
|
+
export interface NextAdminAppProps {
|
|
4
|
+
current: ReactNode;
|
|
5
|
+
adminInterface: AdminInterface;
|
|
6
|
+
setAdminInterface: Dispatch<SetStateAction<AdminInterface>>;
|
|
7
|
+
}
|
|
8
|
+
export declare function NextAdminApp({ current }: NextAdminAppProps): ReactNode;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AdminInterface = "current" | "next";
|
|
2
|
+
type StorageReader = Pick<Storage, "getItem">;
|
|
3
|
+
type StorageWriter = Pick<Storage, "setItem">;
|
|
4
|
+
export declare function parseAdminInterface(value: string | null): AdminInterface;
|
|
5
|
+
export declare function readAdminInterface(storage?: StorageReader): AdminInterface;
|
|
6
|
+
export declare function saveAdminInterface(value: AdminInterface, storage?: StorageWriter): void;
|
|
7
|
+
export declare function useAdminInterface(): {
|
|
8
|
+
adminInterface: AdminInterface;
|
|
9
|
+
setAdminInterface: import("react").Dispatch<import("react").SetStateAction<AdminInterface>>;
|
|
10
|
+
};
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* La nuova interfaccia nasce dietro una preferenza LOCALE al browser.
|
|
2
|
+
*
|
|
3
|
+
* Non è un'impostazione editoriale del sito: salvarla in D1 o in data/site.json la farebbe
|
|
4
|
+
* passare dalla pubblicazione e la accenderebbe anche agli altri amministratori. Qui invece
|
|
5
|
+
* il sito pubblico non riceve nulla. La nuova esperienza è il default, ma ogni browser può
|
|
6
|
+
* tornare all'interfaccia precedente senza cambiare ciò che vedono gli altri amministratori.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { storageKey } from "../../index.js";
|
|
10
|
+
export function parseAdminInterface(value) {
|
|
11
|
+
return value === "current" ? "current" : "next";
|
|
12
|
+
}
|
|
13
|
+
/* La chiave si calcola al momento della lettura, dopo che mountAdmin ha impostato il prefisso
|
|
14
|
+
* del sito. Calcolarla a livello di modulo la bloccherebbe sul prefisso neutro `adminkit`. */
|
|
15
|
+
export function readAdminInterface(storage) {
|
|
16
|
+
try {
|
|
17
|
+
const target = storage ?? (typeof localStorage !== "undefined" ? localStorage : undefined);
|
|
18
|
+
return parseAdminInterface(target?.getItem(storageKey("adminInterface")) ?? null);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "next";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function saveAdminInterface(value, storage) {
|
|
25
|
+
try {
|
|
26
|
+
const target = storage ?? (typeof localStorage !== "undefined" ? localStorage : undefined);
|
|
27
|
+
target?.setItem(storageKey("adminInterface"), value);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* Se localStorage non è disponibile, la scelta resta valida per la sessione corrente. */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function applyAdminInterface(value) {
|
|
34
|
+
if (typeof document !== "undefined") {
|
|
35
|
+
document.documentElement.setAttribute("data-admin-interface", value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function useAdminInterface() {
|
|
39
|
+
const [adminInterface, setAdminInterface] = useState(readAdminInterface);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
applyAdminInterface(adminInterface);
|
|
42
|
+
saveAdminInterface(adminInterface);
|
|
43
|
+
}, [adminInterface]);
|
|
44
|
+
return { adminInterface, setAdminInterface };
|
|
45
|
+
}
|