@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,233 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/* Editor delle pagine.
|
|
3
|
+
*
|
|
4
|
+
* Le gallery non sono più presentate come un form di CMS: la pagina è la sua selezione di
|
|
5
|
+
* fotografie, quindi l'editor mostra direttamente quella selezione. I campi comuni che hanno
|
|
6
|
+
* senso in qualunque pagina (nome, pubblicazione e menu) vivono invece nella testata.
|
|
7
|
+
*
|
|
8
|
+
* Le altre tipologie restano schema-driven: continuano a ricevere automaticamente i campi
|
|
9
|
+
* dichiarati nello schema, ma senza duplicare quelli già spostati nella testata.
|
|
10
|
+
*/
|
|
11
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
12
|
+
import { DndContext, DragOverlay, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors, } from "@dnd-kit/core";
|
|
13
|
+
import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordinates, useSortable, } from "@dnd-kit/sortable";
|
|
14
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
15
|
+
import { ProseField, SchemaForm } from "../../form/index.js";
|
|
16
|
+
import { api } from "../../index.js";
|
|
17
|
+
import { site } from "../site.js";
|
|
18
|
+
import { uniqueSlug } from "../lib/slug";
|
|
19
|
+
import { proseStats } from "../lib/text";
|
|
20
|
+
import { Icon } from "../ui/icons";
|
|
21
|
+
import { GallerySectionsEditor } from "./GallerySectionsEditor";
|
|
22
|
+
import { PortfolioWorksEditor } from "./PortfolioWorksEditor";
|
|
23
|
+
import { AboutEditor } from "./AboutEditor";
|
|
24
|
+
import { WorkshopEditionsEditor } from "./WorkshopEditionsEditor";
|
|
25
|
+
export function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, }) {
|
|
26
|
+
const [titleOpen, setTitleOpen] = useState(false);
|
|
27
|
+
const [visibilityOpen, setVisibilityOpen] = useState(false);
|
|
28
|
+
const [title, setTitle] = useState(String(page.title ?? ""));
|
|
29
|
+
const [slug, setSlug] = useState(page.slug);
|
|
30
|
+
const visibilityRef = useRef(null);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!titleOpen) {
|
|
33
|
+
setTitle(String(page.title ?? ""));
|
|
34
|
+
setSlug(page.slug);
|
|
35
|
+
}
|
|
36
|
+
}, [page.slug, page.title, titleOpen]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!titleOpen && !visibilityOpen)
|
|
39
|
+
return;
|
|
40
|
+
const close = (event) => {
|
|
41
|
+
if (event.key === "Escape") {
|
|
42
|
+
setTitleOpen(false);
|
|
43
|
+
setVisibilityOpen(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
addEventListener("keydown", close);
|
|
47
|
+
return () => removeEventListener("keydown", close);
|
|
48
|
+
}, [titleOpen, visibilityOpen]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!visibilityOpen)
|
|
51
|
+
return;
|
|
52
|
+
const close = (event) => {
|
|
53
|
+
if (!visibilityRef.current?.contains(event.target))
|
|
54
|
+
setVisibilityOpen(false);
|
|
55
|
+
};
|
|
56
|
+
addEventListener("pointerdown", close);
|
|
57
|
+
return () => removeEventListener("pointerdown", close);
|
|
58
|
+
}, [visibilityOpen]);
|
|
59
|
+
const saveTitle = () => {
|
|
60
|
+
const nextTitle = title.trim();
|
|
61
|
+
if (!nextTitle)
|
|
62
|
+
return;
|
|
63
|
+
const taken = pages.filter((item) => item.slug !== page.slug).map((item) => item.slug);
|
|
64
|
+
onChange({
|
|
65
|
+
...page,
|
|
66
|
+
title: nextTitle,
|
|
67
|
+
slug: uniqueSlug(slug || nextTitle, taken),
|
|
68
|
+
});
|
|
69
|
+
setTitleOpen(false);
|
|
70
|
+
};
|
|
71
|
+
const online = page.published !== false;
|
|
72
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pe-head-main", children: [_jsx("button", { type: "button", className: "pe-title", title: "Modifica nome e indirizzo", onClick: () => {
|
|
73
|
+
setTitle(String(page.title ?? ""));
|
|
74
|
+
setSlug(page.slug);
|
|
75
|
+
setVisibilityOpen(false);
|
|
76
|
+
setTitleOpen(true);
|
|
77
|
+
}, children: String(page.title ?? page.slug) }), _jsxs("button", { type: "button", className: "pe-seo-button" + (seoActive ? " is-active" : ""), "aria-pressed": seoActive, onClick: onSeoToggle, children: [_jsx(Icon.search, {}), "SEO"] }), _jsxs("div", { className: "pe-visibility", ref: visibilityRef, children: [_jsxs("button", { type: "button", className: "pe-status" + (online ? " is-online" : " is-offline"), "aria-haspopup": "menu", "aria-expanded": visibilityOpen, onClick: () => setVisibilityOpen((open) => !open), children: [_jsx("span", { className: "pe-status-dot" }), online ? "Online" : "Offline", _jsx("span", { className: "pe-chevron", "aria-hidden": "true", children: "\u2304" })] }), visibilityOpen && (_jsxs("div", { className: "pe-status-menu", role: "menu", children: [_jsx("span", { className: "pe-status-label", children: "Visibilit\u00E0 della pagina" }), _jsxs("button", { type: "button", className: online ? "is-selected" : "", onClick: () => {
|
|
78
|
+
if (!online)
|
|
79
|
+
onChange({ ...page, published: true });
|
|
80
|
+
setVisibilityOpen(false);
|
|
81
|
+
}, children: [_jsx("span", { className: "pe-radio", children: _jsx("span", {}) }), _jsxs("span", { children: [_jsx("strong", { children: "Online" }), _jsx("small", { children: "La pagina \u00E8 pubblicata e accessibile a tutti." })] })] }), _jsxs("button", { type: "button", className: !online ? "is-selected" : "", onClick: () => {
|
|
82
|
+
if (online)
|
|
83
|
+
onChange({ ...page, published: false });
|
|
84
|
+
setVisibilityOpen(false);
|
|
85
|
+
}, children: [_jsx("span", { className: "pe-radio", children: _jsx("span", {}) }), _jsxs("span", { children: [_jsx("strong", { children: "Offline" }), _jsx("small", { children: "La pagina non \u00E8 visibile sul sito." })] })] })] }))] }), _jsxs("label", { className: "pe-menu-toggle", children: [_jsx("input", { type: "checkbox", checked: !!page.inMenu, onChange: (event) => onChange({ ...page, inMenu: event.target.checked }) }), _jsx("span", { children: "Aggiungi al menu" })] })] }), titleOpen && (_jsx("div", { className: "pe-modal", role: "presentation", onMouseDown: (event) => {
|
|
86
|
+
if (event.target === event.currentTarget)
|
|
87
|
+
setTitleOpen(false);
|
|
88
|
+
}, children: _jsxs("div", { className: "pe-modal-card", role: "dialog", "aria-modal": "true", "aria-labelledby": "pe-title-dialog", children: [_jsxs("header", { children: [_jsx("h2", { id: "pe-title-dialog", children: "Modifica pagina" }), _jsx("button", { type: "button", onClick: () => setTitleOpen(false), "aria-label": "Chiudi", children: "\u00D7" })] }), _jsxs("div", { className: "pe-modal-fields", children: [_jsxs("label", { children: [_jsx("span", { children: "Nome pagina" }), _jsx("input", { autoFocus: true, value: title, onChange: (event) => setTitle(event.target.value), onKeyDown: (event) => {
|
|
89
|
+
if (event.key === "Enter")
|
|
90
|
+
saveTitle();
|
|
91
|
+
} })] }), _jsxs("label", { children: [_jsx("span", { children: "Indirizzo pagina" }), _jsxs("div", { className: "pe-url-input", children: [_jsx("span", { children: "/" }), _jsx("input", { value: slug, onChange: (event) => setSlug(event.target.value) }), _jsx("span", { children: "/" })] }), _jsx("small", { children: "I vecchi collegamenti non funzioneranno pi\u00F9 se cambi l\u2019indirizzo." })] })] }), _jsxs("footer", { children: [_jsx("button", { type: "button", className: "ak-btn", onClick: () => setTitleOpen(false), children: "Annulla" }), _jsx("button", { type: "button", className: "ak-btn ak-btn-primary", disabled: !title.trim(), onClick: saveTitle, children: "Aggiorna" })] })] }) }))] }));
|
|
92
|
+
}
|
|
93
|
+
function imageName(src) {
|
|
94
|
+
const raw = src.split("?")[0]?.split("/").filter(Boolean).at(-1) || "Immagine";
|
|
95
|
+
try {
|
|
96
|
+
return decodeURIComponent(raw).replace(/\.[a-z0-9]+$/i, "").replace(/[-_]+/g, " ");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return raw;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function GalleryCard({ item, activeId, overId, activeIndex, overIndex, onReplace, onToggle, onRemove, }) {
|
|
103
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: item.src });
|
|
104
|
+
const isTarget = overId === item.src && activeId !== item.src;
|
|
105
|
+
const insertAfter = isTarget && activeIndex < overIndex;
|
|
106
|
+
const insertBefore = isTarget && activeIndex > overIndex;
|
|
107
|
+
return (_jsxs("figure", { ref: setNodeRef, className: [
|
|
108
|
+
"pe-photo",
|
|
109
|
+
item.hidden ? "is-hidden" : "",
|
|
110
|
+
isDragging ? "is-dragging" : "",
|
|
111
|
+
insertBefore ? "is-insert-before" : "",
|
|
112
|
+
insertAfter ? "is-insert-after" : "",
|
|
113
|
+
].filter(Boolean).join(" "), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsxs("div", { className: "pe-photo-frame", ...attributes, ...listeners, children: [_jsx("img", { src: item.src, alt: imageName(item.src), draggable: false }), item.hidden && _jsx("span", { className: "pe-hidden-label", children: "Nascosta" })] }), _jsxs("div", { className: "pe-photo-actions", children: [_jsx("button", { type: "button", onClick: onReplace, title: "Cambia immagine", "aria-label": "Cambia immagine", children: _jsx(Icon.replace, {}) }), _jsx("button", { type: "button", onClick: onToggle, title: item.hidden ? "Rimetti online" : "Nascondi dalla gallery", "aria-label": item.hidden ? "Rimetti online" : "Nascondi dalla gallery", children: item.hidden ? _jsx(Icon.eye, {}) : _jsx(Icon.eyeOff, {}) }), _jsx("button", { type: "button", className: "is-danger", onClick: onRemove, title: "Rimuovi dalla gallery", "aria-label": "Rimuovi dalla gallery", children: _jsx(Icon.trash, {}) })] }), _jsx("figcaption", { title: imageName(item.src), children: imageName(item.src) })] }));
|
|
114
|
+
}
|
|
115
|
+
function GalleryEditor({ value, onChange, pickMediaFiles, }) {
|
|
116
|
+
const list = Array.isArray(value) ? value : [];
|
|
117
|
+
const [activeId, setActiveId] = useState(null);
|
|
118
|
+
const [overId, setOverId] = useState(null);
|
|
119
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
120
|
+
const activeIndex = activeId ? list.findIndex((item) => item.src === activeId) : -1;
|
|
121
|
+
const overIndex = overId ? list.findIndex((item) => item.src === overId) : -1;
|
|
122
|
+
const activeItem = activeIndex >= 0 ? list[activeIndex] : null;
|
|
123
|
+
const visible = list.filter((item) => !item.hidden).length;
|
|
124
|
+
const add = async () => {
|
|
125
|
+
const picked = await pickMediaFiles?.({ multiple: true });
|
|
126
|
+
if (!picked?.length)
|
|
127
|
+
return;
|
|
128
|
+
const have = new Set(list.map((item) => item.src));
|
|
129
|
+
const additions = picked
|
|
130
|
+
.filter((file) => !have.has(file.url))
|
|
131
|
+
.map((file) => ({ src: file.url, w: file.w ?? null, h: file.h ?? null }));
|
|
132
|
+
if (additions.length)
|
|
133
|
+
onChange([...list, ...additions]);
|
|
134
|
+
};
|
|
135
|
+
const replace = async (index) => {
|
|
136
|
+
const picked = await pickMediaFiles?.({ multiple: false });
|
|
137
|
+
const file = picked?.[0];
|
|
138
|
+
if (!file || file.url === list[index]?.src)
|
|
139
|
+
return;
|
|
140
|
+
if (list.some((item, itemIndex) => itemIndex !== index && item.src === file.url)) {
|
|
141
|
+
alert("Questa foto è già presente nella gallery.");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
onChange(list.map((item, itemIndex) => itemIndex === index
|
|
145
|
+
? { ...item, src: file.url, w: file.w ?? null, h: file.h ?? null }
|
|
146
|
+
: item));
|
|
147
|
+
};
|
|
148
|
+
const finishDrag = ({ active, over }) => {
|
|
149
|
+
setActiveId(null);
|
|
150
|
+
setOverId(null);
|
|
151
|
+
if (!over || active.id === over.id)
|
|
152
|
+
return;
|
|
153
|
+
const from = list.findIndex((item) => item.src === active.id);
|
|
154
|
+
const to = list.findIndex((item) => item.src === over.id);
|
|
155
|
+
if (from >= 0 && to >= 0)
|
|
156
|
+
onChange(arrayMove(list, from, to));
|
|
157
|
+
};
|
|
158
|
+
return (_jsxs("section", { className: "pe-gallery", "aria-label": "Foto della gallery", children: [_jsxs("div", { className: "pe-gallery-bar", children: [_jsxs("div", { children: [_jsxs("strong", { children: [list.length, " ", list.length === 1 ? "foto" : "foto"] }), _jsx("span", { children: list.length === 0
|
|
159
|
+
? "Scegli le immagini da mostrare nella pagina."
|
|
160
|
+
: `${visible} online${visible < list.length ? ` · ${list.length - visible} nascoste` : ""}` })] }), _jsxs("button", { type: "button", className: "pe-add", onClick: () => void add(), children: ["Aggiungi foto ", _jsx(Icon.plus, {})] })] }), list.length === 0 ? (_jsxs("button", { type: "button", className: "pe-gallery-empty", onClick: () => void add(), children: [_jsx(Icon.gallery, {}), _jsx("strong", { children: "La gallery \u00E8 vuota" }), _jsx("span", { children: "Aggiungi le prime fotografie" })] })) : (_jsxs(DndContext, { sensors: sensors, collisionDetection: closestCenter, onDragStart: ({ active }) => setActiveId(String(active.id)), onDragOver: ({ over }) => setOverId(over ? String(over.id) : null), onDragCancel: () => { setActiveId(null); setOverId(null); }, onDragEnd: finishDrag, children: [_jsx(SortableContext, { items: list.map((item) => item.src), strategy: rectSortingStrategy, children: _jsx("div", { className: "pe-photo-grid", children: list.map((item, index) => (_jsx(GalleryCard, { item: item, activeId: activeId, overId: overId, activeIndex: activeIndex, overIndex: overIndex, onReplace: () => void replace(index), onToggle: () => onChange(list.map((candidate, itemIndex) => itemIndex === index
|
|
161
|
+
? { ...candidate, hidden: candidate.hidden ? undefined : true }
|
|
162
|
+
: candidate)), onRemove: () => onChange(list.filter((_, itemIndex) => itemIndex !== index)) }, item.src))) }) }), _jsx(DragOverlay, { children: activeItem && (_jsx("div", { className: "pe-photo-overlay", children: _jsx("img", { src: activeItem.src, alt: "" }) })) })] }))] }));
|
|
163
|
+
}
|
|
164
|
+
export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, }) {
|
|
165
|
+
const [bodyState, setBodyState] = useState("idle");
|
|
166
|
+
const showBody = site().hasBody(page.type);
|
|
167
|
+
const isGallery = page.type === "gallery";
|
|
168
|
+
const isPortfolio = page.type === "portfolio";
|
|
169
|
+
/* APRENDO UNA PAGINA SI RILEGGE DAL REPO, non dal manifest pubblicato. */
|
|
170
|
+
const [loadedFor, setLoadedFor] = useState(null);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (loadedFor === page.slug || !pristine)
|
|
173
|
+
return;
|
|
174
|
+
let alive = true;
|
|
175
|
+
if (showBody)
|
|
176
|
+
setBodyState("loading");
|
|
177
|
+
api.pageBody(page.slug)
|
|
178
|
+
.then((data) => {
|
|
179
|
+
if (!alive)
|
|
180
|
+
return;
|
|
181
|
+
setLoadedFor(page.slug);
|
|
182
|
+
setBodyState("idle");
|
|
183
|
+
onRefresh({
|
|
184
|
+
...page,
|
|
185
|
+
...(data.page ?? {}),
|
|
186
|
+
slug: page.slug,
|
|
187
|
+
_orig: page._orig,
|
|
188
|
+
...(showBody ? { body: data.body } : {}),
|
|
189
|
+
});
|
|
190
|
+
})
|
|
191
|
+
.catch(() => {
|
|
192
|
+
if (alive) {
|
|
193
|
+
setLoadedFor(page.slug);
|
|
194
|
+
setBodyState("error");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return () => { alive = false; };
|
|
198
|
+
}, [page.slug, showBody, pristine, loadedFor]);
|
|
199
|
+
const commonOmissions = useMemo(() => [
|
|
200
|
+
"title", "published", "inMenu",
|
|
201
|
+
"seoTitle", "seoDescription", "ogTitle", "ogDescription", "ogImage",
|
|
202
|
+
], []);
|
|
203
|
+
/* Come la gallery: una pagina portfolio È la sua selezione di lavori, quindi la schermata
|
|
204
|
+
mostra quella e basta. Il `SchemaForm` generico qui elencava anche «Foto», «Edizioni»,
|
|
205
|
+
«Ritratto» e «Riconoscimenti» — campi di altri tipi di pagina, che compaiono perché il
|
|
206
|
+
form disegna tutto ciò che lo schema dichiara e non sa quale tipo si stia modificando.
|
|
207
|
+
Titolo, visibilità e SEO restano dove sono già: la barra in alto e la scheda SEO. */
|
|
208
|
+
if (isPortfolio) {
|
|
209
|
+
return (_jsxs("div", { className: "ed ed-portfolio", children: [bodyState === "error" && (_jsx("p", { className: "pe-load-error", children: "Non sono riuscito ad aggiornare i dati della pagina dal sito." })), _jsx(PortfolioWorksEditor, { value: page.works, onChange: (works) => onChange({ ...page, works }), pickMediaFiles: pickMediaFiles })] }));
|
|
210
|
+
}
|
|
211
|
+
/* Stessa ragione delle due qui sopra: la pagina "chi sono" ha un'anatomia sua — biografia,
|
|
212
|
+
ritratto, riconoscimenti — e il form generico ci metteva accanto «Lavori», «Edizioni» e
|
|
213
|
+
«Sezioni», cioè i campi delle altre tipologie. Il corpo markdown è la biografia, quindi la
|
|
214
|
+
textarea in fondo non serve più: sta dentro l'editor, in cima, dov'è il suo posto. */
|
|
215
|
+
if (page.type === "about") {
|
|
216
|
+
return (_jsx("div", { className: "ed ed-about", children: _jsx(AboutEditor, { page: page, onChange: onChange, bodyState: bodyState, pickMediaFiles: pickMediaFiles }) }));
|
|
217
|
+
}
|
|
218
|
+
/* La pagina workshop: l'introduzione e le edizioni, nell'ordine in cui escono sul sito —
|
|
219
|
+
l'introduzione apre la colonna dei testi, sopra la prima locandina (WORKSHOP.md). Il corpo
|
|
220
|
+
markdown è quell'introduzione, quindi non c'è nessuna textarea in fondo: sta in cima, dove
|
|
221
|
+
la si legge. */
|
|
222
|
+
if (page.type === "workshop") {
|
|
223
|
+
return (_jsxs("div", { className: "ed ed-workshop", children: [_jsxs("section", { className: "wsh-intro", "aria-label": "Introduzione", children: [_jsxs("header", { className: "pw-head", children: [_jsxs("div", { children: [_jsx("h3", { children: "Introduzione" }), _jsx("p", { children: "Il testo che apre la pagina, sopra la prima edizione." })] }), _jsx("span", { className: "ed-count", children: bodyState === "loading" ? "Carico…" : proseStats(String(page.body ?? "")) })] }), bodyState === "error" && (_jsx("p", { className: "ak-error", children: "Non sono riuscito a caricare il testo dal sito. Ricarica prima di modificarlo." })), _jsx(ProseField, { className: "pw-field ed-prose", value: typeof page.body === "string" ? page.body : "", placeholder: "Racconta i workshop.\nVai a capo per iniziare un paragrafo nuovo.", onChange: (body) => onChange({ ...page, body }) })] }), _jsx(WorkshopEditionsEditor, { value: page.workshops, onChange: (workshops) => onChange({ ...page, workshops }), pickMediaFiles: pickMediaFiles })] }));
|
|
224
|
+
}
|
|
225
|
+
if (isGallery) {
|
|
226
|
+
/* Le foto si modificano DOVE stanno: dentro il loro blocco, che è anche dove vivono nei dati
|
|
227
|
+
(`sections[].images`) e dove la griglia esce sul sito. Non c'è più un pannello staccato in
|
|
228
|
+
fondo — c'era finché le foto stavano in un campo della pagina, e insieme a lui è sparito
|
|
229
|
+
il caso in cui la stessa griglia si vedeva due volte. */
|
|
230
|
+
return (_jsxs("div", { className: "ed ed-gallery", children: [bodyState === "error" && (_jsx("p", { className: "pe-load-error", children: "Non sono riuscito ad aggiornare i dati della pagina dal sito." })), _jsx(GallerySectionsEditor, { value: page.sections, onChange: (sections) => onChange({ ...page, sections }), renderGallery: (images, onImages) => (_jsx(GalleryEditor, { value: images, onChange: onImages, pickMediaFiles: pickMediaFiles })), pickMediaFiles: pickMediaFiles })] }));
|
|
231
|
+
}
|
|
232
|
+
return (_jsxs("div", { className: "ed", children: [_jsx(SchemaForm, { shape: site().page, value: page, omit: commonOmissions, onChange: (next) => onChange({ ...next, slug: page.slug }), pickMedia: pickMedia, pickMediaFiles: pickMediaFiles }), showBody && (_jsxs("fieldset", { className: "ak-group", children: [_jsx("legend", { className: "ak-legend", children: "testo della pagina" }), bodyState === "loading" && _jsx("p", { className: "ak-hint", children: "Carico\u2026" }), bodyState === "error" && _jsx("p", { className: "ak-error", children: "Non sono riuscito a caricare il testo." }), _jsx("textarea", { className: "ak-input ak-textarea", rows: 12, value: typeof page.body === "string" ? page.body : "", onChange: (event) => onChange({ ...page, body: event.target.value }) })] }))] }));
|
|
233
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AssetFile } from "../../index.js";
|
|
2
|
+
import type { PageModel } from "../lib/pages";
|
|
3
|
+
import type { SiteSettings } from "../lib/settings";
|
|
4
|
+
export declare function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFiles, siteSettings, }: {
|
|
5
|
+
page: PageModel;
|
|
6
|
+
onChange: (next: PageModel) => void;
|
|
7
|
+
onRefresh: (next: PageModel) => void;
|
|
8
|
+
pristine: boolean;
|
|
9
|
+
pickMediaFiles?: (o: {
|
|
10
|
+
multiple: boolean;
|
|
11
|
+
}) => Promise<AssetFile[] | null>;
|
|
12
|
+
siteSettings?: SiteSettings | null;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* Vista SEO della singola pagina.
|
|
3
|
+
*
|
|
4
|
+
* È una vista dell'editor, non un form separato: modifica lo stesso PageModel e usa lo stesso
|
|
5
|
+
* «Salva e pubblica». Le anteprime applicano gli stessi ripieghi del partial head.html:
|
|
6
|
+
* Open Graph -> SEO -> titolo editoriale / descrizione generale.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from "react";
|
|
9
|
+
import { api } from "../../index.js";
|
|
10
|
+
import { Icon } from "../ui/icons";
|
|
11
|
+
import { site } from "../site.js";
|
|
12
|
+
/* ⚠️ Non costanti di modulo: `site()` è disponibile solo dopo `mountAdmin`, e un `const` qui
|
|
13
|
+
* verrebbe valutato all'import — cioè prima. Si leggono dentro il componente.
|
|
14
|
+
*
|
|
15
|
+
* `DEFAULT_DESCRIPTION` era «Luxury Hospitality & Food Photographer | Rome, Italy», cioè la
|
|
16
|
+
* descrizione di un cliente scritta dentro il motore. Il lucchetto NON l'aveva vista: cerca
|
|
17
|
+
* nomi, e quella riga non ne conteneva. Ora non serve più un valore di ripiego inventato — se
|
|
18
|
+
* il sito non ha né descrizione breve né descrizione lunga, l'anteprima mostra la stringa
|
|
19
|
+
* vuota, che è la verità: manca. */
|
|
20
|
+
function text(value) {
|
|
21
|
+
return typeof value === "string" ? value : "";
|
|
22
|
+
}
|
|
23
|
+
function automaticImage(page) {
|
|
24
|
+
const sections = Array.isArray(page.sections) ? page.sections : [];
|
|
25
|
+
const sectionImage = sections.find((item) => item?.type === "image" && text(item?.image));
|
|
26
|
+
if (sectionImage)
|
|
27
|
+
return text(sectionImage.image);
|
|
28
|
+
// Le foto della gallery stanno DENTRO il loro blocco fra le sezioni, non in un campo della
|
|
29
|
+
// pagina: stessa catena di head.html, che qui va rispecchiata o l'anteprima mente.
|
|
30
|
+
const galleryImage = sections
|
|
31
|
+
.flatMap((item) => (Array.isArray(item?.images) ? item.images : []))
|
|
32
|
+
.find((item) => item?.hidden !== true && text(item?.src));
|
|
33
|
+
if (galleryImage)
|
|
34
|
+
return text(galleryImage.src);
|
|
35
|
+
const works = Array.isArray(page.works) ? page.works : [];
|
|
36
|
+
const work = works.find((item) => text(item?.cover));
|
|
37
|
+
if (work)
|
|
38
|
+
return text(work.cover);
|
|
39
|
+
const workshops = Array.isArray(page.workshops) ? page.workshops : [];
|
|
40
|
+
const workshop = workshops.find((item) => text(item?.image));
|
|
41
|
+
if (workshop)
|
|
42
|
+
return text(workshop.image);
|
|
43
|
+
return text(page.portrait);
|
|
44
|
+
}
|
|
45
|
+
function FieldHelp({ children, }) {
|
|
46
|
+
return _jsx("p", { className: "seo-help", children: children });
|
|
47
|
+
}
|
|
48
|
+
function Count({ value, recommended, max }) {
|
|
49
|
+
return (_jsxs("span", { className: "seo-count" + (value.length > recommended ? " is-long" : ""), children: [value.length, "/", max, " caratteri"] }));
|
|
50
|
+
}
|
|
51
|
+
export function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFiles, siteSettings, }) {
|
|
52
|
+
const [loadedFor, setLoadedFor] = useState(null);
|
|
53
|
+
const [loadError, setLoadError] = useState(false);
|
|
54
|
+
/* Arrivando direttamente a /seo la pagina normale non viene montata, quindi la lettura dal
|
|
55
|
+
repo si fa anche qui: l'anteprima non deve partire dal manifest pubblicato qualche minuto
|
|
56
|
+
prima. */
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (loadedFor === page.slug || !pristine)
|
|
59
|
+
return;
|
|
60
|
+
let alive = true;
|
|
61
|
+
api.pageBody(page.slug)
|
|
62
|
+
.then((data) => {
|
|
63
|
+
if (!alive)
|
|
64
|
+
return;
|
|
65
|
+
setLoadedFor(page.slug);
|
|
66
|
+
setLoadError(false);
|
|
67
|
+
onRefresh({
|
|
68
|
+
...page,
|
|
69
|
+
...(data.page ?? {}),
|
|
70
|
+
slug: page.slug,
|
|
71
|
+
_orig: page._orig,
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
.catch(() => {
|
|
75
|
+
if (alive) {
|
|
76
|
+
setLoadedFor(page.slug);
|
|
77
|
+
setLoadError(true);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return () => { alive = false; };
|
|
81
|
+
}, [page.slug, pristine, loadedFor]);
|
|
82
|
+
const seoTitle = text(page.seoTitle);
|
|
83
|
+
const seoDescription = text(page.seoDescription);
|
|
84
|
+
const ogTitle = text(page.ogTitle);
|
|
85
|
+
const ogDescription = text(page.ogDescription);
|
|
86
|
+
const explicitImage = text(page.ogImage);
|
|
87
|
+
const globalTitle = siteSettings?.siteTitle || site().name;
|
|
88
|
+
const globalDescription = siteSettings?.shortDescription || siteSettings?.siteDescription || "";
|
|
89
|
+
const searchTitle = seoTitle || `${text(page.title)} — ${globalTitle}`;
|
|
90
|
+
const searchDescription = seoDescription || globalDescription;
|
|
91
|
+
const socialTitle = ogTitle || searchTitle;
|
|
92
|
+
const socialDescription = ogDescription || searchDescription;
|
|
93
|
+
const socialImage = explicitImage || automaticImage(page);
|
|
94
|
+
const pageUrl = `${site().publicBase}/${page.slug}/`;
|
|
95
|
+
const complete = !!seoTitle.trim() && !!seoDescription.trim() && !!socialImage;
|
|
96
|
+
const chooseImage = async () => {
|
|
97
|
+
const selected = await pickMediaFiles?.({ multiple: false });
|
|
98
|
+
if (selected?.[0])
|
|
99
|
+
onChange({ ...page, ogImage: selected[0].url });
|
|
100
|
+
};
|
|
101
|
+
return (_jsxs("section", { className: "seo-editor", children: [_jsxs("header", { className: "seo-intro", children: [_jsxs("div", { children: [_jsx("span", { className: "seo-kicker", children: "Impostazioni della pagina" }), _jsx("h2", { children: "Ottimizzazione SEO" }), _jsx("p", { children: "Controlla come questa pagina appare nei motori di ricerca e quando viene condivisa." })] }), _jsxs("span", { className: "seo-complete" + (complete ? " is-complete" : ""), children: [_jsx("span", { children: complete ? "✓" : "!" }), complete ? "SEO completa" : "Da completare"] })] }), loadError && (_jsx("p", { className: "pe-load-error", children: "Non sono riuscito ad aggiornare i dati SEO dal sito." })), _jsxs("section", { className: "seo-section", children: [_jsxs("div", { className: "seo-section-heading", children: [_jsx("span", { children: "Motori di ricerca" }), _jsxs("div", { children: [_jsx("h3", { children: "Anteprima del risultato" }), _jsx("p", { children: "Il contenuto si aggiorna mentre scrivi." })] })] }), _jsxs("div", { className: "seo-row seo-preview-row", children: [_jsx("span", { className: "seo-row-label", children: "Anteprima" }), _jsxs("div", { className: "seo-search-preview", "aria-label": "Anteprima del risultato di ricerca", children: [_jsx("span", { className: "seo-search-site", children: globalTitle }), _jsx("span", { className: "seo-search-url", children: pageUrl }), _jsx("strong", { children: searchTitle }), _jsx("p", { children: searchDescription })] }), _jsx(FieldHelp, { children: "\u00C8 una simulazione del risultato. Il motore di ricerca pu\u00F2 adattarne il testo." })] }), _jsxs("div", { className: "seo-row", children: [_jsxs("label", { className: "seo-field", children: [_jsx("span", { children: "Titolo SEO" }), _jsx("input", { value: seoTitle, maxLength: 120, placeholder: `${text(page.title)} — ${globalTitle}`, onChange: (event) => onChange({ ...page, seoTitle: event.target.value }) }), _jsx(Count, { value: seoTitle, recommended: 60, max: 120 })] }), _jsx(FieldHelp, { children: "Un titolo descrittivo e unico. Per una lettura completa nei risultati \u00E8 preferibile restare entro circa 60 caratteri." })] }), _jsxs("div", { className: "seo-row", children: [_jsxs("label", { className: "seo-field", children: [_jsx("span", { children: "Descrizione SEO" }), _jsx("textarea", { rows: 5, value: seoDescription, maxLength: 255, placeholder: "Descrivi il contenuto della pagina e il valore che offre.", onChange: (event) => onChange({ ...page, seoDescription: event.target.value }) }), _jsx(Count, { value: seoDescription, recommended: 160, max: 255 })] }), _jsx(FieldHelp, { children: "Riassumi la pagina con parole naturali e pertinenti. I primi 160 caratteri sono quelli che hanno pi\u00F9 probabilit\u00E0 di essere mostrati per intero." })] })] }), _jsxs("section", { className: "seo-section", children: [_jsxs("div", { className: "seo-section-heading", children: [_jsx("span", { children: "Open Graph" }), _jsxs("div", { children: [_jsx("h3", { children: "Anteprima della condivisione" }), _jsx("p", { children: "Usata da social, chat e applicazioni quando viene condiviso il link." })] })] }), _jsxs("div", { className: "seo-row seo-preview-row", children: [_jsx("span", { className: "seo-row-label", children: "Anteprima" }), _jsxs("article", { className: "seo-og-preview", "aria-label": "Anteprima Open Graph", children: [_jsx("div", { className: "seo-og-image", children: socialImage ? (_jsx("img", { src: socialImage, alt: "" })) : (_jsxs("span", { children: [_jsx(Icon.media, {}), " Nessuna immagine"] })) }), _jsxs("div", { className: "seo-og-copy", children: [_jsx("span", { children: site().publicBase.replace(/^https?:\/\//, "").toUpperCase() }), _jsx("strong", { children: socialTitle }), _jsx("p", { children: socialDescription })] })] }), _jsx(FieldHelp, { children: "L\u2019anteprima usa i campi Open Graph; quelli vuoti ereditano automaticamente i valori SEO." })] }), _jsxs("div", { className: "seo-row", children: [_jsxs("label", { className: "seo-field", children: [_jsx("span", { children: "Titolo Open Graph" }), _jsx("input", { value: ogTitle, maxLength: 120, placeholder: searchTitle, onChange: (event) => onChange({ ...page, ogTitle: event.target.value }) }), _jsx(Count, { value: ogTitle, recommended: 70, max: 120 })] }), _jsx(FieldHelp, { children: "Lascia vuoto per usare il titolo SEO. Puoi renderlo pi\u00F9 editoriale per la condivisione." })] }), _jsxs("div", { className: "seo-row", children: [_jsxs("label", { className: "seo-field", children: [_jsx("span", { children: "Descrizione Open Graph" }), _jsx("textarea", { rows: 4, value: ogDescription, maxLength: 300, placeholder: searchDescription, onChange: (event) => onChange({ ...page, ogDescription: event.target.value }) }), _jsx(Count, { value: ogDescription, recommended: 200, max: 300 })] }), _jsx(FieldHelp, { children: "Lascia vuoto per usare la descrizione SEO. Mantienila breve per evitare tagli nelle card." })] }), _jsxs("div", { className: "seo-row", children: [_jsxs("div", { className: "seo-field", children: [_jsx("span", { children: "Immagine Open Graph" }), _jsxs("div", { className: "seo-image-picker", children: [socialImage ? _jsx("img", { src: socialImage, alt: "" }) : _jsx("div", { children: _jsx(Icon.media, {}) }), _jsxs("div", { children: [_jsx("button", { type: "button", className: "ak-btn", onClick: () => void chooseImage(), children: explicitImage ? "Cambia immagine" : "Scegli immagine" }), explicitImage && (_jsx("button", { type: "button", className: "ak-btn ak-btn-quiet", onClick: () => onChange({ ...page, ogImage: "" }), children: "Usa automatica" })), _jsx("small", { children: explicitImage ? "Immagine personalizzata" : socialImage ? "Prima immagine pubblica della pagina" : "Nessun ripiego disponibile" })] })] })] }), _jsx(FieldHelp, { children: "Una foto orizzontale funziona meglio nelle condivisioni. Se non ne scegli una, viene usata la prima immagine pubblica della pagina." })] })] })] }));
|
|
102
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PageModel } from "../lib/pages";
|
|
2
|
+
export declare function PagesList({ pages, current, onEdit, onChange, onRenamed, }: {
|
|
3
|
+
pages: PageModel[];
|
|
4
|
+
current?: string;
|
|
5
|
+
onEdit: (slug: string) => void;
|
|
6
|
+
onChange: (next: PageModel[]) => void;
|
|
7
|
+
/** Una rinomina cambia l'indirizzo: se la pagina è quella aperta nell'editor, la rotta punta
|
|
8
|
+
* a uno slug che non esiste più e l'editor resterebbe vuoto. Lo sa solo chi tiene la rotta. */
|
|
9
|
+
onRenamed?: (from: string, to: string) => void;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* Elenco delle pagine — la seconda colonna. Due gruppi: quelle nel menu e le altre.
|
|
3
|
+
*
|
|
4
|
+
* Da quando vive in una colonna accanto all'editor, la riga INTERA apre la pagina: il bottone
|
|
5
|
+
* "Modifica" era un bersaglio piccolo dentro una riga che sembrava già cliccabile, e con l'editor
|
|
6
|
+
* a fianco aprire è il gesto normale, non un'azione da confermare.
|
|
7
|
+
*
|
|
8
|
+
* Il riordino usa @dnd-kit invece di portare i gestori HTML5 del vecchio admin: quelli erano il
|
|
9
|
+
* codice più delicato di app.js (dragover, midpoint del cursore, dataTransfer) e non c'era
|
|
10
|
+
* niente da conservarci. Qui l'ordine è lo stato, e il peso lo ricalcola `positionWeight`.
|
|
11
|
+
*/
|
|
12
|
+
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
13
|
+
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
|
14
|
+
import { SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, arrayMove } from "@dnd-kit/sortable";
|
|
15
|
+
import { KeyboardSensor } from "@dnd-kit/core";
|
|
16
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
17
|
+
import { RowMenu } from "./PageActions";
|
|
18
|
+
import { uniqueSlug } from "../lib/slug";
|
|
19
|
+
import { Icon } from "../ui/icons";
|
|
20
|
+
const ICONS = {
|
|
21
|
+
gallery: Icon.gallery, page: Icon.doc, contact: Icon.mail,
|
|
22
|
+
};
|
|
23
|
+
function Row({ p, pages, current, onEdit, onChange, onRemove, onDuplicate }) {
|
|
24
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: p.slug });
|
|
25
|
+
const TypeIcon = ICONS[String(p.type)] ?? Icon.doc;
|
|
26
|
+
return (_jsxs("li", { ref: setNodeRef, className: "pg-row" + (isDragging ? " is-dragging" : "") + (p.slug === current ? " is-on" : ""), style: { transform: CSS.Transform.toString(transform), transition }, children: [_jsx("button", { className: "pg-handle", ...attributes, ...listeners, title: "Trascina per riordinare", children: _jsx(Icon.grip, {}) }), _jsxs("button", { className: "pg-open", onClick: () => onEdit(p.slug), children: [_jsx("span", { className: "pg-icon", children: _jsx(TypeIcon, {}) }), _jsx("span", { className: "pg-title", children: String(p.title ?? p.slug) }), p.published === false && (_jsx("span", { className: "pg-off", title: "nascosta: non va online", children: _jsx(Icon.eyeOff, {}) }))] }), _jsx(RowMenu, { page: p, pages: pages, onChange: (next) => onChange(p.slug, next), onRemove: onRemove, onDuplicate: onDuplicate })] }));
|
|
27
|
+
}
|
|
28
|
+
function Group({ title, items, all, current, onEdit, onReorder, onChange, onRemove, onDuplicate, }) {
|
|
29
|
+
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
30
|
+
const onDragEnd = (e) => {
|
|
31
|
+
const { active, over } = e;
|
|
32
|
+
if (!over || active.id === over.id)
|
|
33
|
+
return;
|
|
34
|
+
onReorder(items.findIndex((p) => p.slug === active.id), items.findIndex((p) => p.slug === over.id));
|
|
35
|
+
};
|
|
36
|
+
return (_jsxs("section", { className: "pg-group", children: [_jsx("h2", { className: "pg-group-title", children: title }), items.length === 0 && _jsx("p", { className: "pg-empty", children: "nessuna" }), _jsx(DndContext, { sensors: sensors, collisionDetection: closestCenter, modifiers: [restrictToVerticalAxis], onDragEnd: onDragEnd, children: _jsx(SortableContext, { items: items.map((p) => p.slug), strategy: verticalListSortingStrategy, children: _jsx("ul", { className: "pg-list", children: items.map((p) => (_jsx(Row, { p: p, pages: all, current: current, onEdit: onEdit, onChange: onChange, onRemove: () => onRemove(p.slug), onDuplicate: () => onDuplicate(p.slug) }, p.slug))) }) }) })] }));
|
|
37
|
+
}
|
|
38
|
+
export function PagesList({ pages, current, onEdit, onChange, onRenamed, }) {
|
|
39
|
+
const inMenu = pages.filter((p) => !!p.inMenu);
|
|
40
|
+
const notInMenu = pages.filter((p) => !p.inMenu);
|
|
41
|
+
const reorder = (group, from, to) => {
|
|
42
|
+
const moved = arrayMove(group, from, to);
|
|
43
|
+
const other = group === inMenu ? notInMenu : inMenu;
|
|
44
|
+
onChange(group === inMenu ? [...moved, ...other] : [...other, ...moved]);
|
|
45
|
+
};
|
|
46
|
+
// Cambiare `inMenu` da qui deve anche spostare la pagina nell'altro gruppo, altrimenti
|
|
47
|
+
// resterebbe dov'era finché non si ricarica — e il peso, che dipende dal gruppo, sarebbe
|
|
48
|
+
// calcolato su una posizione che l'utente non vede.
|
|
49
|
+
//
|
|
50
|
+
// La pagina da sostituire si cerca con lo slug di PARTENZA (`from`), non con quello della
|
|
51
|
+
// versione nuova: la rinomina cambia anche lo slug, quindi cercando `next.slug` non trovava
|
|
52
|
+
// nessuna riga e l'elenco tornava identico — il nome nuovo spariva senza un errore.
|
|
53
|
+
const replace = (from, next) => {
|
|
54
|
+
onChange(pages.map((p) => (p.slug === from ? next : p)));
|
|
55
|
+
if (next.slug !== from)
|
|
56
|
+
onRenamed?.(from, next.slug);
|
|
57
|
+
};
|
|
58
|
+
const remove = (slug) => onChange(pages.filter((p) => p.slug !== slug));
|
|
59
|
+
const duplicate = (slug) => {
|
|
60
|
+
const src = pages.find((p) => p.slug === slug);
|
|
61
|
+
if (!src)
|
|
62
|
+
return;
|
|
63
|
+
const copia = {
|
|
64
|
+
...structuredClone(src),
|
|
65
|
+
slug: uniqueSlug(`${src.slug}-copia`, pages.map((p) => p.slug)),
|
|
66
|
+
title: `${src.title} (copia)`,
|
|
67
|
+
inMenu: false, // una copia non entra nel menu da sola
|
|
68
|
+
published: false, // né va online prima che qualcuno l'abbia guardata
|
|
69
|
+
_orig: null, // pagina nuova: niente da cui conservare
|
|
70
|
+
};
|
|
71
|
+
const i = pages.indexOf(src);
|
|
72
|
+
onChange([...pages.slice(0, i + 1), copia, ...pages.slice(i + 1)]);
|
|
73
|
+
};
|
|
74
|
+
const shared = { all: pages, current, onEdit, onChange: replace, onRemove: remove, onDuplicate: duplicate };
|
|
75
|
+
return (_jsxs("div", { className: "pg", children: [_jsx(Group, { title: "Nel menu", items: inMenu, ...shared, onReorder: (f, t) => reorder(inMenu, f, t) }), _jsx(Group, { title: "Fuori dal menu", items: notInMenu, ...shared, onReorder: (f, t) => reorder(notInMenu, f, t) })] }));
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AssetFile } from "../../index.js";
|
|
2
|
+
import { normalizeWorks, type Work, type WorkCard, type WorkCardType, type WorkInfo } from "../../normalize.js";
|
|
3
|
+
export type { WorkCardType, WorkInfo, WorkCard, Work };
|
|
4
|
+
export { normalizeWorks };
|
|
5
|
+
type Picker = (o: {
|
|
6
|
+
multiple: boolean;
|
|
7
|
+
}) => Promise<AssetFile[] | null>;
|
|
8
|
+
export declare function PortfolioWorksEditor({ value, onChange, pickMediaFiles }: {
|
|
9
|
+
value: unknown;
|
|
10
|
+
onChange: (next: Work[]) => void;
|
|
11
|
+
pickMediaFiles?: Picker;
|
|
12
|
+
}): import("react").JSX.Element;
|