@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,107 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* Le sezioni di una pagina gallery: i blocchi editoriali E la griglia delle fotografie, in un
|
|
3
|
+
* elenco solo, nell'ordine in cui si vedono sul sito.
|
|
4
|
+
*
|
|
5
|
+
* Quattro tipi soltanto e con layout progettati: Header, Testo, Immagine e «Foto della gallery».
|
|
6
|
+
* L'utente può comporli e riordinarli; nel blocco delle foto può regolare colonne e distanze
|
|
7
|
+
* entro domini che template e animazione sanno governare, senza introdurre stili liberi.
|
|
8
|
+
*
|
|
9
|
+
* Il blocco delle foto NON si aggiunge dal menu: **se la pagina è una gallery, il blocco c'è**.
|
|
10
|
+
* È il posto in cui le foto vivono (`section.images`), si scelgono e si riordinano — quindi
|
|
11
|
+
* eliminarlo elimina le foto, senza nessun codice che vada a svuotare un secondo campo. Quando
|
|
12
|
+
* il front matter non lo contiene ancora lo si sintetizza per la vista, con l'id COSTANTE
|
|
13
|
+
* dichiarato nello schema: generarlo come gli altri (`Date.now()` + random) darebbe a dnd-kit
|
|
14
|
+
* una chiave nuova a ogni render, la card verrebbe rimontata di continuo e trascinarla sarebbe
|
|
15
|
+
* impossibile.
|
|
16
|
+
*
|
|
17
|
+
* Uno per pagina: `gallery.js` è costruito attorno a UNA griglia (la classe della vista sta su
|
|
18
|
+
* `.rg`, `cells` è una lista piatta), quindi un secondo blocco non avrebbe niente da governare.
|
|
19
|
+
* Non essendoci la voce nel menu, un secondo non si può nemmeno creare.
|
|
20
|
+
*
|
|
21
|
+
* Il riordino NON disegna più una barra di inserimento. Ce n'era una, blu, e serviva a dire
|
|
22
|
+
* dove sarebbe caduta la sezione trascinata: era un tratto in un colore fuori palette, e
|
|
23
|
+
* soprattutto ripeteva una cosa che `verticalListSortingStrategy` mostra già da sé allontanando
|
|
24
|
+
* gli elementi. Toglierla ha portato via anche lo stato che la alimentava (`activeId`/`overId`
|
|
25
|
+
* e i due handler che li aggiornavano a ogni movimento del puntatore).
|
|
26
|
+
*
|
|
27
|
+
* L'aspetto è quello dell'editor dei lavori del portfolio, e non per somiglianza: sono gli
|
|
28
|
+
* stessi valori, dichiarati una volta in `theme.css` (`--ak-ground`, `--ak-inset`, `--ak-lift-*`).
|
|
29
|
+
*/
|
|
30
|
+
import { useEffect, useRef, useState } from "react";
|
|
31
|
+
import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core";
|
|
32
|
+
import { SortableContext, arrayMove, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
33
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
34
|
+
import { GALLERY_BLOCK_ID, emptySection } from "../../index.js";
|
|
35
|
+
import { normalizeSections, withGalleryBlock } from "../../normalize.js";
|
|
36
|
+
import { ParagraphEditor } from "../ui/ParagraphEditor";
|
|
37
|
+
import { Icon } from "../ui/icons";
|
|
38
|
+
function sectionId() {
|
|
39
|
+
return `section-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
|
|
40
|
+
}
|
|
41
|
+
/* `normalizeSections` e `withGalleryBlock` vivono in `src/normalize.ts`: logica pura, e la
|
|
42
|
+
verifica sui contenuti veri di ogni sito gira in Node, dove React non c'è. */
|
|
43
|
+
const normalized = normalizeSections;
|
|
44
|
+
function SectionIcon({ type }) {
|
|
45
|
+
if (type === "gallery")
|
|
46
|
+
return _jsx(Icon.gallery, {});
|
|
47
|
+
return type === "image" ? _jsx(Icon.media, {}) : type === "header" ? _jsx(Icon.pages, {}) : _jsx(Icon.doc, {});
|
|
48
|
+
}
|
|
49
|
+
function labelOf(type) {
|
|
50
|
+
return type === "header" ? "Header"
|
|
51
|
+
: type === "text" ? "Testo"
|
|
52
|
+
: type === "gallery" ? "Foto della gallery"
|
|
53
|
+
: "Immagine";
|
|
54
|
+
}
|
|
55
|
+
function SortableSection({ section, index, onPatch, onDuplicate, onRemove, renderGallery, pickMediaFiles, }) {
|
|
56
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: section.id });
|
|
57
|
+
const isGallery = section.type === "gallery";
|
|
58
|
+
const chooseImage = async () => {
|
|
59
|
+
const selected = await pickMediaFiles?.({ multiple: false });
|
|
60
|
+
const file = selected?.[0];
|
|
61
|
+
if (file)
|
|
62
|
+
onPatch({ image: file.url, w: file.w ?? null, h: file.h ?? null });
|
|
63
|
+
};
|
|
64
|
+
return (_jsxs("article", { ref: setNodeRef, className: ["gs-card", `is-${section.type}`, isDragging ? "is-dragging" : ""]
|
|
65
|
+
.filter(Boolean).join(" "), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("header", { className: "gs-card-head", children: [_jsx("button", { type: "button", className: "gs-drag", title: "Trascina per riordinare", "aria-label": `Riordina sezione ${labelOf(section.type)}`, ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), _jsx("span", { className: "gs-card-icon", children: _jsx(SectionIcon, { type: section.type }) }), _jsx("strong", { children: labelOf(section.type) }), _jsx("span", { className: "gs-position", children: index + 1 }), !isGallery && (_jsx("button", { type: "button", className: "gs-head-action", onClick: onDuplicate, children: "Duplica" })), (!isGallery || section.images.length > 0) && (_jsx("button", { type: "button", className: "gs-head-action is-danger", onClick: onRemove, children: "Elimina" }))] }), isGallery && (_jsx("div", { className: "gs-gallery", children: renderGallery?.(section.images, (images) => onPatch({ images })) })), section.type === "header" && (_jsxs("div", { className: "gs-fields", children: [_jsxs("label", { children: [_jsxs("span", { children: ["Occhiello ", _jsx("small", { children: "facoltativo" })] }), _jsx("input", { value: section.eyebrow, maxLength: 120, placeholder: "Es. Fotografia gastronomica", onChange: (event) => onPatch({ eyebrow: event.target.value }) })] }), _jsxs("label", { children: [_jsx("span", { children: "Titolo" }), _jsx("input", { value: section.heading, maxLength: 200, placeholder: "Titolo della sezione", onChange: (event) => onPatch({ heading: event.target.value }) })] }), _jsxs("label", { children: [_jsxs("span", { children: ["Introduzione ", _jsx("small", { children: "facoltativa" })] }), _jsx("textarea", { rows: 4, value: section.lead, maxLength: 800, placeholder: "Una breve apertura sotto il titolo.", onChange: (event) => onPatch({ lead: event.target.value }) })] })] })), section.type === "text" && (_jsx("div", { className: "gs-fields", children: _jsxs("label", { children: [_jsx("span", { children: "Testo" }), _jsx(ParagraphEditor, { value: section.paragraphs, onChange: (paragraphs) => onPatch({ paragraphs }) }), _jsx("small", { children: "Seleziona una parola per metterla in grassetto, in corsivo o per collegarla." })] }) })), section.type === "image" && (_jsxs("div", { className: "gs-image-editor", children: [_jsx("button", { type: "button", className: "gs-image-preview", onClick: () => void chooseImage(), children: section.image ? _jsx("img", { src: section.image, alt: "" }) : (_jsxs("span", { children: [_jsx(Icon.media, {}), " Scegli un\u2019immagine"] })) }), _jsxs("div", { className: "gs-image-fields", children: [_jsxs("div", { className: "gs-image-actions", children: [_jsx("button", { type: "button", className: "btn-soft", onClick: () => void chooseImage(), children: section.image ? "Cambia immagine" : "Scegli immagine" }), section.image && (_jsx("button", { type: "button", className: "btn-soft is-quiet", onClick: () => onPatch({ image: "", w: null, h: null }), children: "Rimuovi" }))] }), _jsxs("label", { children: [_jsx("span", { children: "Testo alternativo" }), _jsx("input", { value: section.alt, maxLength: 300, placeholder: "Descrive l\u2019immagine a chi non pu\u00F2 vederla", onChange: (event) => onPatch({ alt: event.target.value }) })] }), _jsxs("label", { children: [_jsxs("span", { children: ["Didascalia ", _jsx("small", { children: "facoltativa" })] }), _jsx("input", { value: section.caption, maxLength: 300, onChange: (event) => onPatch({ caption: event.target.value }) })] }), _jsxs("div", { className: "gs-layout", children: [_jsx("span", { children: "Dimensione" }), _jsxs("div", { className: "seg", role: "group", "aria-label": "Dimensione dell\u2019immagine", children: [_jsx("button", { type: "button", className: "seg-btn" + (section.layout === "wide" ? " is-on" : ""), onClick: () => onPatch({ layout: "wide" }), children: "Larga" }), _jsx("button", { type: "button", className: "seg-btn" + (section.layout === "contained" ? " is-on" : ""), onClick: () => onPatch({ layout: "contained" }), children: "Contenuta" })] })] })] })] }))] }));
|
|
66
|
+
}
|
|
67
|
+
export function GallerySectionsEditor({ value, onChange, renderGallery, pickMediaFiles, }) {
|
|
68
|
+
const sections = withGalleryBlock(normalized(value));
|
|
69
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
70
|
+
const menuRef = useRef(null);
|
|
71
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!menuOpen)
|
|
74
|
+
return;
|
|
75
|
+
const close = (event) => {
|
|
76
|
+
if (!menuRef.current?.contains(event.target))
|
|
77
|
+
setMenuOpen(false);
|
|
78
|
+
};
|
|
79
|
+
addEventListener("pointerdown", close);
|
|
80
|
+
return () => removeEventListener("pointerdown", close);
|
|
81
|
+
}, [menuOpen]);
|
|
82
|
+
const add = (type) => {
|
|
83
|
+
onChange([...sections, emptySection(type, sectionId())]);
|
|
84
|
+
setMenuOpen(false);
|
|
85
|
+
};
|
|
86
|
+
/* Eliminare il blocco delle foto elimina le foto: stanno lì dentro. Non è un effetto
|
|
87
|
+
collaterale da compensare altrove — è il modello — ma va detto prima, con il numero. */
|
|
88
|
+
const remove = (section) => {
|
|
89
|
+
if (section.type === "gallery" && section.images.length) {
|
|
90
|
+
const n = section.images.length;
|
|
91
|
+
const ok = confirm(`Elimini il blocco con le ${n} foto di questa pagina?\n\n` +
|
|
92
|
+
`Le foto vengono tolte dalla pagina. Restano nei media e si possono rimettere.`);
|
|
93
|
+
if (!ok)
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
onChange(sections.filter((item) => item.id !== section.id));
|
|
97
|
+
};
|
|
98
|
+
const finishDrag = ({ active, over }) => {
|
|
99
|
+
if (!over || active.id === over.id)
|
|
100
|
+
return;
|
|
101
|
+
const from = sections.findIndex((section) => section.id === active.id);
|
|
102
|
+
const to = sections.findIndex((section) => section.id === over.id);
|
|
103
|
+
if (from >= 0 && to >= 0)
|
|
104
|
+
onChange(arrayMove(sections, from, to));
|
|
105
|
+
};
|
|
106
|
+
return (_jsxs("section", { className: "gs-editor", "aria-label": "Sezioni introduttive", children: [_jsxs("div", { className: "gs-add-wrap", ref: menuRef, children: [_jsxs("button", { type: "button", className: "gs-add", "aria-haspopup": "menu", "aria-expanded": menuOpen, onClick: () => setMenuOpen((open) => !open), children: ["Aggiungi sezione ", _jsx(Icon.plus, {})] }), menuOpen && (_jsxs("div", { className: "gs-add-menu", role: "menu", children: [_jsxs("button", { type: "button", onClick: () => add("header"), children: [_jsx("span", { children: _jsx(Icon.pages, {}) }), _jsxs("span", { children: [_jsx("strong", { children: "Header" }), _jsx("small", { children: "Titolo e breve introduzione" })] })] }), _jsxs("button", { type: "button", onClick: () => add("text"), children: [_jsx("span", { children: _jsx(Icon.doc, {}) }), _jsxs("span", { children: [_jsx("strong", { children: "Testo" }), _jsx("small", { children: "Uno o pi\u00F9 paragrafi" })] })] }), _jsxs("button", { type: "button", onClick: () => add("image"), children: [_jsx("span", { children: _jsx(Icon.media, {}) }), _jsxs("span", { children: [_jsx("strong", { children: "Immagine" }), _jsx("small", { children: "Foto editoriale con didascalia" })] })] })] }))] }), !!sections.length && (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: finishDrag, children: _jsx(SortableContext, { items: sections.map((section) => section.id), strategy: verticalListSortingStrategy, children: _jsx("div", { className: "gs-list", children: sections.map((section, index) => (_jsx(SortableSection, { section: section, index: index, renderGallery: renderGallery, pickMediaFiles: pickMediaFiles, onPatch: (patch) => onChange(sections.map((item) => item.id === section.id ? { ...item, ...patch } : item)), onDuplicate: () => onChange(sections.flatMap((item) => item.id === section.id ? [item, { ...item, id: sectionId() }] : [item])), onRemove: () => remove(section) }, section.id))) }) }) }))] }));
|
|
107
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Data = Record<string, unknown>;
|
|
2
|
+
export declare function HomeEditor({ data, group, onChange, pickMedia }: {
|
|
3
|
+
data: Data;
|
|
4
|
+
group: string;
|
|
5
|
+
onChange: (next: Data) => void;
|
|
6
|
+
pickMedia?: (o: {
|
|
7
|
+
multiple: boolean;
|
|
8
|
+
}) => Promise<string[] | null>;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/* La home, una schermata alla volta.
|
|
3
|
+
*
|
|
4
|
+
* ── L'idea ─────────────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* Come per gallery, portfolio, "chi sono" e workshop: l'editor mostra LA PAGINA, non i campi
|
|
6
|
+
* della pagina. Qui però la pagina è una sola e lunga sei schermate, quindi la seconda colonna
|
|
7
|
+
* resta l'elenco delle schermate — quello c'era già ed è giusto — e a cambiare è cosa si vede
|
|
8
|
+
* dentro: prima `SchemaForm` disegnava riquadri annidati con dentro elenchi generici (frecce
|
|
9
|
+
* ↑↓, «+ Aggiungi», una `legend` per ogni livello), cioè la forma dei DATI invece di quella
|
|
10
|
+
* della schermata che quei dati compongono.
|
|
11
|
+
*
|
|
12
|
+
* Ogni schermata ha ora la sua impaginazione: la foto dov'è grande, i testi in colonna a misura
|
|
13
|
+
* di lettura, gli elenchi come righe con la maniglia.
|
|
14
|
+
*
|
|
15
|
+
* ── LA REGOLA DA NON ROMPERE: si applicano PATCH ───────────────────────────────────────────
|
|
16
|
+
* Ogni modifica riscrive il blocco con `{ ...blocco, campo: valore }` e MAI ricostruendolo da
|
|
17
|
+
* zero. Non è pignoleria: il serializzatore della home (`nested()` in schema/src/home.ts) emette
|
|
18
|
+
* tutto ciò che trova nell'oggetto, quindi un campo che nel file non c'è, se qui viene
|
|
19
|
+
* "normalizzato" a stringa vuota, comparirebbe nel file al primo salvataggio — e il round-trip
|
|
20
|
+
* (`test/home-content.test.js`) diventerebbe rosso. Gli editor delle pagine possono permettersi
|
|
21
|
+
* di normalizzare perché lì l'`encode` toglie di nuovo i campi vuoti; qui no.
|
|
22
|
+
*
|
|
23
|
+
* Per lo stesso motivo le letture usano un fallback (`str`, `list`) ma non lo riscrivono: serve
|
|
24
|
+
* a disegnare il campo vuoto, non a riempire il file.
|
|
25
|
+
*/
|
|
26
|
+
import { useContext, useState } from "react";
|
|
27
|
+
import { DndContext, closestCenter } from "@dnd-kit/core";
|
|
28
|
+
import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy, rectSortingStrategy, } from "@dnd-kit/sortable";
|
|
29
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
30
|
+
import { ProseField, ProsePages } from "../../form/index.js";
|
|
31
|
+
import { ParagraphEditor } from "../ui/ParagraphEditor";
|
|
32
|
+
import { MediaSlot } from "../ui/MediaSlot";
|
|
33
|
+
import { OpenCard } from "../ui/OpenCard";
|
|
34
|
+
import { useDragSensors } from "../ui/dnd";
|
|
35
|
+
import { movedOpenIndex, removedOpenIndex } from "../lib/reorder";
|
|
36
|
+
import { Icon } from "../ui/icons";
|
|
37
|
+
import { site } from "../site.js";
|
|
38
|
+
const str = (value) => (typeof value === "string" ? value : "");
|
|
39
|
+
const list = (value) => (Array.isArray(value) ? value : []);
|
|
40
|
+
const strings = (value) => list(value).map(str);
|
|
41
|
+
const obj = (value) => (value && typeof value === "object" ? value : {});
|
|
42
|
+
/* ---- Pezzi comuni alle schermate ------------------------------------------------------------ */
|
|
43
|
+
/* Il NOME della schermata non si ripete qui: sta già nella testata del pannello, che lo prende
|
|
44
|
+
dal gruppo scelto nella seconda colonna. Qui resta la riga che dice cos'è quella schermata
|
|
45
|
+
sul sito — l'unica cosa che la testata non può sapere. */
|
|
46
|
+
function Screen({ hint, children }) {
|
|
47
|
+
return (_jsxs("section", { className: "hm", children: [hint && _jsx("p", { className: "hm-hint", children: hint }), children] }));
|
|
48
|
+
}
|
|
49
|
+
function Block({ title, action, children }) {
|
|
50
|
+
return (_jsxs("section", { className: "hm-block", children: [(title || action) && (_jsxs("header", { className: "pw-block-head", children: [title ? _jsx("h4", { children: title }) : _jsx("span", {}), action] })), children] }));
|
|
51
|
+
}
|
|
52
|
+
function Field({ label, note, children }) {
|
|
53
|
+
return (_jsxs("label", { className: "pw-label", children: [_jsxs("span", { children: [label, note && _jsxs("small", { children: [" ", note] })] }), children] }));
|
|
54
|
+
}
|
|
55
|
+
function Text({ value, onChange, placeholder, maxLength, big }) {
|
|
56
|
+
return (_jsx("input", { className: "pw-field" + (big ? " pw-field-title" : ""), value: value, placeholder: placeholder, maxLength: maxLength, onChange: (event) => onChange(event.target.value) }));
|
|
57
|
+
}
|
|
58
|
+
/** Una foto della home: sempre scelta dalla libreria, mai un indirizzo battuto a mano. */
|
|
59
|
+
function Photo({ value, label, note, ratio = "wide", onPick, onClear }) {
|
|
60
|
+
return (_jsxs("div", { className: "hm-photo", children: [_jsxs("span", { className: "hm-photo-label", children: [label, note && _jsxs("small", { children: [" ", note] })] }), _jsx(MediaSlot, { value: value, ratio: ratio, empty: "Scegli una foto", onPick: onPick, onClear: onClear })] }));
|
|
61
|
+
}
|
|
62
|
+
/* Un elenco di righe di testo con la maniglia: le righe del titolo, i numeri, i paragrafi
|
|
63
|
+
liberi. Le chiavi sono posizionali, come negli altri elenchi del pannello. */
|
|
64
|
+
function Rows({ items, ids, onChange, addLabel, onAdd, empty, render }) {
|
|
65
|
+
const sensors = useDragSensors();
|
|
66
|
+
const finish = ({ active, over }) => {
|
|
67
|
+
if (!over || active.id === over.id)
|
|
68
|
+
return;
|
|
69
|
+
onChange(arrayMove(items, ids.indexOf(String(active.id)), ids.indexOf(String(over.id))));
|
|
70
|
+
};
|
|
71
|
+
return (_jsxs(_Fragment, { children: [items.length === 0 && empty && _jsx("p", { className: "pw-empty-note", children: empty }), _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: finish, children: _jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: _jsx("ul", { className: "hm-rows", children: items.map((item, index) => (_jsx(Row, { id: ids[index], onRemove: () => onChange(items.filter((_, i) => i !== index)), children: render(item, index, (next) => onChange(items.map((x, i) => (i === index ? next : x)))) }, ids[index]))) }) }) }), _jsxs("button", { type: "button", className: "pw-add-quiet", onClick: onAdd, children: [_jsx(Icon.plus, {}), " ", addLabel] })] }));
|
|
72
|
+
}
|
|
73
|
+
function Row({ id, onRemove, children }) {
|
|
74
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
|
75
|
+
return (_jsxs("li", { ref: setNodeRef, className: "hm-row" + (isDragging ? " is-dragging" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsx("button", { type: "button", className: "pw-grip", "aria-label": "Riordina", ...attributes, ...listeners, children: _jsx(Icon.grip, {}) }), children, _jsx("button", { type: "button", className: "pw-icon-btn is-danger", "aria-label": "Elimina", onClick: onRemove, children: _jsx(Icon.trash, {}) })] }));
|
|
76
|
+
}
|
|
77
|
+
/* ---- Apertura ------------------------------------------------------------------------------- */
|
|
78
|
+
/* Le righe del titolo si spezzano a mano — il testo non va MAI a capo da solo — e la riga più
|
|
79
|
+
* lunga detta il corpo del carattere di TUTTE (css/home/hero.css). Un elenco di caselle non lo
|
|
80
|
+
* racconta: l'anteprima qui accanto mostra le interruzioni come si vedranno, che è l'unico modo
|
|
81
|
+
* di accorgersi che una riga è troppo lunga prima di pubblicare. */
|
|
82
|
+
function HeroScreen({ value, onChange, pick }) {
|
|
83
|
+
const hero = obj(value.hero);
|
|
84
|
+
const patch = (next) => onChange({ ...value, hero: { ...hero, ...next } });
|
|
85
|
+
const title = strings(hero.title);
|
|
86
|
+
const narrow = strings(hero.narrow);
|
|
87
|
+
return (_jsx(Screen, { hint: "La prima schermata: la foto a tutta pagina e il titolo sopra.", children: _jsxs("div", { className: "hm-split", children: [_jsx(Photo, { value: str(hero.photo), label: "Foto", onPick: () => pick((photo) => patch({ photo })), onClear: () => patch({ photo: "" }) }), _jsxs("div", { className: "hm-side", children: [_jsxs(Block, { title: "Titolo", children: [_jsx("div", { className: "hm-title-preview", "aria-hidden": "true", children: (title.length ? title : ["Il titolo della home"]).map((line, index) => (_jsx("span", { className: title.length ? "" : "is-empty", children: line }, index))) }), _jsx(Rows, { items: title, ids: title.map((_, i) => `riga-${i}`), onChange: (next) => patch({ title: next }), onAdd: () => patch({ title: [...title, ""] }), addLabel: "Aggiungi riga", empty: "Nessuna riga: il titolo non comparirebbe.", render: (line, index, set) => (_jsx("input", { className: "pw-field", value: line, maxLength: 30, placeholder: `Riga ${index + 1}`, "aria-label": `Riga ${index + 1} del titolo`, onChange: (event) => set(event.target.value) })) })] }), _jsxs(Block, { title: "Titolo su schermi stretti", children: [_jsx("p", { className: "pw-empty-note", children: "Su un telefono le righe larghe rimpicciolirebbero tutto il titolo, quindi qui si spezza pi\u00F9 corto. Lasciandolo vuoto si usano comunque le righe qui sopra." }), _jsx(Rows, { items: narrow, ids: narrow.map((_, i) => `stretta-${i}`), onChange: (next) => patch({ narrow: next }), onAdd: () => patch({ narrow: [...narrow, ""] }), addLabel: "Aggiungi riga", render: (line, index, set) => (_jsx("input", { className: "pw-field", value: line, maxLength: 20, placeholder: `Riga ${index + 1}`, "aria-label": `Riga ${index + 1} del titolo su schermi stretti`, onChange: (event) => set(event.target.value) })) })] }), _jsx(Block, { children: _jsx(Field, { label: "Invito allo scroll", note: "in fondo alla schermata", children: _jsx(Text, { value: str(hero.scroll), maxLength: 20, placeholder: "Scroll", onChange: (scroll) => patch({ scroll }) }) }) })] })] }) }));
|
|
88
|
+
}
|
|
89
|
+
/* ---- Statistiche ----------------------------------------------------------------------------- */
|
|
90
|
+
function StatsScreen({ value, onChange, pick }) {
|
|
91
|
+
const stats = obj(value.stats);
|
|
92
|
+
const bg = obj(stats.bg);
|
|
93
|
+
const items = list(stats.items).map(obj);
|
|
94
|
+
const patch = (next) => onChange({ ...value, stats: { ...stats, ...next } });
|
|
95
|
+
return (_jsxs(Screen, { hint: "I numeri ruotano uno alla volta, sopra la foto di sfondo.", children: [_jsxs("div", { className: "hm-photos", children: [_jsx(Photo, { value: str(bg.desktop), label: "Sfondo", note: "schermi larghi", onPick: () => pick((desktop) => patch({ bg: { ...bg, desktop } })), onClear: () => patch({ bg: { ...bg, desktop: "" } }) }), _jsx(Photo, { value: str(bg.mobile), label: "Sfondo", note: "telefono", ratio: "portrait", onPick: () => pick((mobile) => patch({ bg: { ...bg, mobile } })), onClear: () => patch({ bg: { ...bg, mobile: "" } }) })] }), _jsx(Block, { title: "Numeri", children: _jsx(Rows, { items: items, ids: items.map((_, i) => `numero-${i}`), onChange: (next) => patch({ items: next }), onAdd: () => patch({ items: [...items, { num: "", label: "" }] }), addLabel: "Aggiungi numero", empty: "Nessun numero: la schermata resterebbe con il solo sfondo.", render: (item, _index, set) => (_jsxs(_Fragment, { children: [_jsx("input", { className: "pw-field hm-num", value: str(item.num), maxLength: 12, placeholder: "15+", "aria-label": "Numero", onChange: (event) => set({ ...item, num: event.target.value }) }), _jsx("input", { className: "pw-field", value: str(item.label), maxLength: 60, placeholder: "Years of Experience", "aria-label": "Etichetta", onChange: (event) => set({ ...item, label: event.target.value }) })] })) }) })] }));
|
|
96
|
+
}
|
|
97
|
+
/* ---- Chi sono ------------------------------------------------------------------------------- */
|
|
98
|
+
function AboutScreen({ value, onChange, pick }) {
|
|
99
|
+
const about = obj(value.about);
|
|
100
|
+
const patch = (next) => onChange({ ...value, about: { ...about, ...next } });
|
|
101
|
+
return (_jsx(Screen, { hint: "Il ritratto e il testo che lo accompagna.", children: _jsxs("div", { className: "hm-split", children: [_jsx(Photo, { value: str(about.portrait), label: "Ritratto", ratio: "portrait", onPick: () => pick((portrait) => patch({ portrait })), onClear: () => patch({ portrait: "" }) }), _jsx("div", { className: "hm-side", children: _jsxs(Block, { children: [_jsx(Field, { label: "Apertura", children: _jsx(ProseField, { className: "pw-field pw-textarea", rows: 3, value: str(about.lead), maxLength: 600, onChange: (lead) => patch({ lead }) }) }), _jsx(Field, { label: "Paragrafi", children: _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 8, value: strings(about.paragraphs), onChange: (paragraphs) => patch({ paragraphs }) }) }), _jsx(Field, { label: "Frase-manifesto", note: "la riga in evidenza in fondo", children: _jsx(ProseField, { className: "pw-field pw-textarea", rows: 2, value: str(about.statement), maxLength: 300, onChange: (statement) => patch({ statement }) }) })] }) })] }) }));
|
|
102
|
+
}
|
|
103
|
+
/* ---- What you got --------------------------------------------------------------------------- */
|
|
104
|
+
function ReachScreen({ value, onChange, pick }) {
|
|
105
|
+
const reach = obj(value.reach);
|
|
106
|
+
const patch = (next) => onChange({ ...value, reach: { ...reach, ...next } });
|
|
107
|
+
return (_jsx(Screen, { hint: "La foto, il titolo e i territori.", children: _jsxs("div", { className: "hm-split", children: [_jsx(Photo, { value: str(reach.image), label: "Foto", onPick: () => pick((image) => patch({ image })), onClear: () => patch({ image: "" }) }), _jsx("div", { className: "hm-side", children: _jsxs(Block, { children: [_jsx(Field, { label: "Titolo", children: _jsx(Text, { big: true, value: str(reach.title), maxLength: 120, onChange: (title) => patch({ title }) }) }), _jsx(Field, { label: "Sottotitolo", children: _jsx(Text, { value: str(reach.subtitle), maxLength: 160, onChange: (subtitle) => patch({ subtitle }) }) }), _jsx(Field, { label: "Paragrafi", children: _jsx(ParagraphEditor, { className: "pw-field pw-textarea", rows: 7, value: strings(reach.paragraphs), onChange: (paragraphs) => patch({ paragraphs }) }) })] }) })] }) }));
|
|
108
|
+
}
|
|
109
|
+
/* ---- Le porte verso le altre pagine --------------------------------------------------------- */
|
|
110
|
+
/** L'indirizzo di una porta: si sceglie fra le pagine del sito, non si batte a mano. */
|
|
111
|
+
function PagePicker({ value, onChange }) {
|
|
112
|
+
const pages = useContext(ProsePages);
|
|
113
|
+
const known = pages.some((page) => page.href === value);
|
|
114
|
+
return (_jsxs("select", { className: "pw-field", value: value, onChange: (event) => onChange(event.target.value), children: [_jsx("option", { value: "", children: "Scegli una pagina\u2026" }), !known && value && _jsx("option", { value: value, children: value }), pages.map((page) => _jsx("option", { value: page.href, children: page.label }, page.href))] }));
|
|
115
|
+
}
|
|
116
|
+
function GenresScreen({ value, onChange, pick }) {
|
|
117
|
+
const genres = obj(value.genres);
|
|
118
|
+
const items = list(genres.items).map(obj);
|
|
119
|
+
const [openAt, setOpenAt] = useState(null);
|
|
120
|
+
const sensors = useDragSensors();
|
|
121
|
+
const ids = items.map((_, index) => `porta-${index}`);
|
|
122
|
+
const replace = (next, nextOpen) => {
|
|
123
|
+
setOpenAt(nextOpen);
|
|
124
|
+
onChange({ ...value, genres: { ...genres, items: next } });
|
|
125
|
+
};
|
|
126
|
+
const patchAt = (index, next) => onChange({ ...value, genres: { ...genres, items: items.map((item, i) => (i === index ? next : item)) } });
|
|
127
|
+
const finish = ({ active, over }) => {
|
|
128
|
+
if (!over || active.id === over.id)
|
|
129
|
+
return;
|
|
130
|
+
const from = ids.indexOf(String(active.id));
|
|
131
|
+
const to = ids.indexOf(String(over.id));
|
|
132
|
+
if (from < 0 || to < 0)
|
|
133
|
+
return;
|
|
134
|
+
replace(arrayMove(items, from, to), movedOpenIndex(openAt, from, to));
|
|
135
|
+
};
|
|
136
|
+
return (_jsxs(Screen, { hint: "In home si vedono le prime tre. In fondo a ogni gallery si vedono le tre diverse da quella che si sta guardando: per cambiare quali compaiono in home, cambia l\u2019ordine.", children: [_jsxs("div", { className: "pw-head hm-head-action", children: [_jsx("span", {}), _jsxs("button", { type: "button", className: "btn-soft is-primary", onClick: () => replace([...items, { image: "", title: "", text: "", href: "" }], items.length), children: [_jsx(Icon.plus, {}), " Aggiungi porta"] })] }), _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: finish, children: _jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children: _jsx("div", { className: "pw-list", children: items.map((item, index) => (_jsx(OpenCard, { id: ids[index], index: index, total: items.length, open: openAt === index, thumb: str(item.image), title: str(item.title), empty: "Porta senza titolo",
|
|
137
|
+
/* Le prime tre sono quelle che si vedono in home: si dice qui, dove si decide
|
|
138
|
+
l'ordine, invece che in una nota che nessuno collega alle righe. */
|
|
139
|
+
meta: index < 3 ? "In home" : str(item.href), kind: "questa porta", onToggle: () => setOpenAt(openAt === index ? null : index), actions: _jsx("button", { type: "button", className: "btn-soft is-danger", onClick: () => replace(items.filter((_, i) => i !== index), removedOpenIndex(openAt, index)), children: "Elimina" }), children: _jsxs("div", { className: "pw-identity", children: [_jsx(MediaSlot, { value: str(item.image), ratio: "cover", empty: "Scegli la foto", onPick: () => pick((image) => patchAt(index, { ...item, image })), onClear: () => patchAt(index, { ...item, image: "" }) }), _jsxs("div", { className: "pw-identity-fields", children: [_jsx(Field, { label: "Titolo", children: _jsx(Text, { big: true, value: str(item.title), maxLength: 40, onChange: (title) => patchAt(index, { ...item, title }) }) }), _jsx(Field, { label: "Testo", children: _jsx(ProseField, { className: "pw-field pw-textarea", rows: 3, value: str(item.text), maxLength: 400, onChange: (text) => patchAt(index, { ...item, text }) }) }), _jsx(Field, { label: "Pagina di destinazione", children: _jsx(PagePicker, { value: str(item.href), onChange: (href) => patchAt(index, { ...item, href }) }) })] })] }) }, ids[index]))) }) }) })] }));
|
|
140
|
+
}
|
|
141
|
+
/* ---- Who you are ---------------------------------------------------------------------------- */
|
|
142
|
+
function Shot({ id, url, onRemove }) {
|
|
143
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
|
144
|
+
return (_jsxs("figure", { ref: setNodeRef, className: "hm-shot" + (isDragging ? " is-dragging" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsx("div", { className: "hm-shot-frame", ...attributes, ...listeners, children: _jsx("img", { src: url, alt: "", draggable: false }) }), _jsx("button", { type: "button", className: "hm-shot-remove", "aria-label": "Togli questa foto", onClick: onRemove, children: _jsx(Icon.trash, {}) })] }));
|
|
145
|
+
}
|
|
146
|
+
function ClosingScreen({ value, onChange, pickMany }) {
|
|
147
|
+
const closing = obj(value.closing);
|
|
148
|
+
const images = strings(closing.images);
|
|
149
|
+
const patch = (next) => onChange({ ...value, closing: { ...closing, ...next } });
|
|
150
|
+
const sensors = useDragSensors();
|
|
151
|
+
const ids = images.map((url, index) => `${url}-${index}`);
|
|
152
|
+
const finish = ({ active, over }) => {
|
|
153
|
+
if (!over || active.id === over.id)
|
|
154
|
+
return;
|
|
155
|
+
patch({ images: arrayMove(images, ids.indexOf(String(active.id)), ids.indexOf(String(over.id))) });
|
|
156
|
+
};
|
|
157
|
+
return (_jsxs(Screen, { hint: "Il testo di chiusura e il riquadro di foto che ciclano.", children: [_jsxs(Block, { children: [_jsx(Field, { label: "Titolo", children: _jsx(Text, { big: true, value: str(closing.title), maxLength: 120, onChange: (title) => patch({ title }) }) }), _jsx(Field, { label: "Testo", children: _jsx(ProseField, { className: "pw-field pw-textarea", rows: 6, value: str(closing.text), maxLength: 1200, onChange: (text) => patch({ text }) }) }), _jsxs("div", { className: "hm-two", children: [_jsx(Field, { label: "Etichetta del link", children: _jsx(Text, { value: str(closing.label), maxLength: 40, onChange: (label) => patch({ label }) }) }), _jsx(Field, { label: "Pagina di destinazione", children: _jsx(PagePicker, { value: str(closing.href), onChange: (href) => patch({ href }) }) })] })] }), _jsx(Block, { title: "Foto che ciclano", action: _jsxs("button", { type: "button", className: "pw-add-quiet", onClick: () => pickMany((urls) => patch({ images: [...images, ...urls.filter((url) => !images.includes(url))] })), children: [_jsx(Icon.plus, {}), " Aggiungi foto"] }), children: images.length === 0 ? (_jsx("p", { className: "pw-empty-note", children: "Nessuna foto: il riquadro resterebbe vuoto." })) : (_jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragEnd: finish, children: _jsx(SortableContext, { items: ids, strategy: rectSortingStrategy, children: _jsx("div", { className: "hm-shots", children: images.map((url, index) => (_jsx(Shot, { id: ids[index], url: url, onRemove: () => patch({ images: images.filter((_, i) => i !== index) }) }, ids[index]))) }) }) })) })] }));
|
|
158
|
+
}
|
|
159
|
+
/* ---- Contatto -------------------------------------------------------------------------------- */
|
|
160
|
+
function ContactScreen({ value, onChange }) {
|
|
161
|
+
const contact = obj(value.contact);
|
|
162
|
+
const patch = (next) => onChange({ ...value, contact: { ...contact, ...next } });
|
|
163
|
+
return (_jsxs(Screen, { hint: "L\u2019ultima schermata: l\u2019invito, i recapiti e il modulo. Le richieste arrivano per email e restano in \u00ABRichieste\u00BB.", children: [_jsxs(Block, { children: [_jsx(Field, { label: "Titolo", children: _jsx(Text, { big: true, value: str(contact.title), maxLength: 160, onChange: (title) => patch({ title }) }) }), _jsx(Field, { label: "Testo", children: _jsx(ProseField, { className: "pw-field pw-textarea", rows: 5, value: str(contact.text), maxLength: 900, onChange: (text) => patch({ text }) }) })] }), _jsxs(Block, { title: "Recapiti", children: [_jsx("p", { className: "pw-empty-note", children: "Sono i recapiti del sito: compaiono qui e nella pagina \u00ABchi sono\u00BB. Quelli lasciati vuoti non vengono mostrati." }), _jsxs("div", { className: "hm-two", children: [_jsx(Field, { label: "Email", children: _jsx(Text, { value: str(contact.email), maxLength: 120, placeholder: "info@esempio.it", onChange: (email) => patch({ email }) }) }), _jsx(Field, { label: "Telefono", children: _jsx(Text, { value: str(contact.phone), maxLength: 40, onChange: (phone) => patch({ phone }) }) }), _jsx(Field, { label: "Dove", children: _jsx(Text, { value: str(contact.city), maxLength: 80, placeholder: "Citt\u00E0, Paese", onChange: (city) => patch({ city }) }) }), _jsx(Field, { label: "Instagram", note: "indirizzo completo", children: _jsx(Text, { value: str(contact.instagram), maxLength: 200, placeholder: "https://instagram.com/\u2026", onChange: (instagram) => patch({ instagram }) }) })] })] }), _jsx(Block, { title: "Il modulo", children: _jsxs("div", { className: "hm-two", children: [_jsx(Field, { label: "Etichetta del bottone", children: _jsx(Text, { value: str(contact.submit), maxLength: 40, placeholder: "Send", onChange: (submit) => patch({ submit }) }) }), _jsx(Field, { label: "Messaggio di conferma", note: "lo legge chi ha appena scritto", children: _jsx(Text, { value: str(contact.done), maxLength: 200, onChange: (done) => patch({ done }) }) })] }) })] }));
|
|
164
|
+
}
|
|
165
|
+
/* ---- SEO ------------------------------------------------------------------------------------- */
|
|
166
|
+
function SeoScreen({ value, onChange }) {
|
|
167
|
+
const title = str(value.title);
|
|
168
|
+
const tagline = str(value.tagline);
|
|
169
|
+
return (_jsx(Screen, { hint: "Come il sito si presenta nella scheda del browser e nei risultati di ricerca.", children: _jsxs(Block, { children: [_jsxs("div", { className: "seo-search-preview", "aria-label": "Anteprima del risultato di ricerca", children: [_jsx("span", { className: "seo-search-site", children: title || "Titolo del sito" }), _jsx("span", { className: "seo-search-url", children: site().publicBase.replace(/^https?:\/\//, "") }), _jsx("strong", { children: title || "Titolo del sito" }), _jsx("p", { children: tagline || "La descrizione che compare sotto al titolo." })] }), _jsx("p", { className: "pw-empty-note", children: "\u00C8 una simulazione: il motore di ricerca pu\u00F2 adattarne il testo." }), _jsx(Field, { label: "Titolo del sito", children: _jsx(Text, { value: title, maxLength: 140, onChange: (next) => onChange({ ...value, title: next }) }) }), _jsx(Field, { label: "Sottotitolo", note: "compare anche nel piede del sito", children: _jsx(Text, { value: tagline, maxLength: 140, onChange: (next) => onChange({ ...value, tagline: next }) }) })] }) }));
|
|
170
|
+
}
|
|
171
|
+
/* ---- La schermata scelta ---------------------------------------------------------------------- */
|
|
172
|
+
export function HomeEditor({ data, group, onChange, pickMedia }) {
|
|
173
|
+
/* Una foto per volta, dalla libreria. Il selettore restituisce un elenco: qui interessa la
|
|
174
|
+
prima, e le schermate non devono saperlo. */
|
|
175
|
+
const pick = (then) => {
|
|
176
|
+
void pickMedia?.({ multiple: false }).then((picked) => {
|
|
177
|
+
if (picked?.length)
|
|
178
|
+
then(picked[0]);
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
const pickMany = (then) => {
|
|
182
|
+
void pickMedia?.({ multiple: true }).then((picked) => {
|
|
183
|
+
if (picked?.length)
|
|
184
|
+
then(picked);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
return (_jsxs("div", { className: "ed ed-home", children: [group === "Apertura" && _jsx(HeroScreen, { value: data, onChange: onChange, pick: pick }), group === "Statistiche" && _jsx(StatsScreen, { value: data, onChange: onChange, pick: pick }), group === "Chi sono" && _jsx(AboutScreen, { value: data, onChange: onChange, pick: pick }), group === "What you got" && _jsx(ReachScreen, { value: data, onChange: onChange, pick: pick }), group === "Generi" && _jsx(GenresScreen, { value: data, onChange: onChange, pick: pick }), group === "Who you are" && _jsx(ClosingScreen, { value: data, onChange: onChange, pickMany: pickMany }), group === "Contatto" && _jsx(ContactScreen, { value: data, onChange: onChange }), group === "SEO" && _jsx(SeoScreen, { value: data, onChange: onChange })] }));
|
|
188
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ContactRequestRow } from "../../index.js";
|
|
2
|
+
import type { Booking } from "../lib/bookings";
|
|
3
|
+
import type { DeliverySummary } from "../lib/deliveries";
|
|
4
|
+
import type { SiteFeatures } from "../lib/settings";
|
|
5
|
+
interface Props {
|
|
6
|
+
deliveries: DeliverySummary[] | null;
|
|
7
|
+
expiring: number;
|
|
8
|
+
requests: ContactRequestRow[] | null;
|
|
9
|
+
pending: number;
|
|
10
|
+
features: SiteFeatures | null;
|
|
11
|
+
bookings: Booking[] | null;
|
|
12
|
+
bookingsPending: number;
|
|
13
|
+
bookingsError: string | null;
|
|
14
|
+
go: (to: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function Overview({ deliveries, expiring, requests, pending, features, bookings, bookingsPending, bookingsError, go, }: Props): import("react").JSX.Element;
|
|
17
|
+
export {};
|