@cardinali-studio/adminkit 1.0.0 → 2.1.0-rc.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 +8 -1
- package/dist/api.js +1 -1
- package/dist/app/App.js +8 -3
- package/dist/app/app.css +27 -0
- package/dist/app/lib/pages.d.ts +11 -0
- package/dist/app/lib/pages.js +17 -1
- package/dist/app/routes/PageActions.js +1 -2
- package/dist/app/routes/PageEditor.d.ts +12 -2
- package/dist/app/routes/PageEditor.js +82 -14
- package/dist/app/routes/PageSeoEditor.d.ts +4 -1
- package/dist/app/routes/PageSeoEditor.js +2 -2
- package/dist/app/routes/PagesList.d.ts +5 -1
- package/dist/app/routes/PagesList.js +12 -6
- package/dist/app/site.d.ts +21 -4
- package/dist/app/site.js +3 -0
- package/dist/form/SchemaForm.d.ts +3 -1
- package/dist/form/SchemaForm.js +14 -8
- package/dist/form/fields/objectList.js +9 -6
- package/dist/form/registry.d.ts +2 -0
- package/dist/functions/_shared/media-content.js +10 -2
- package/dist/functions/_shared/translations.js +18 -0
- package/dist/functions/api/pages/get.js +34 -9
- package/dist/functions/api/pages.js +116 -8
- package/dist/functions/site.js +15 -0
- package/dist/i18n.d.ts +26 -0
- package/dist/i18n.js +119 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export interface RepoPage {
|
|
|
44
44
|
/** Lo `sha` del blob su GitHub: identifica la VERSIONE di questo file, non il suo contenuto. */
|
|
45
45
|
sha: string;
|
|
46
46
|
page: Record<string, unknown>;
|
|
47
|
+
/** Le sibling dichiarate dal sito, senza caricare il loro contenuto nell'elenco. */
|
|
48
|
+
translations?: Record<string, {
|
|
49
|
+
exists: boolean;
|
|
50
|
+
sha?: string;
|
|
51
|
+
}>;
|
|
47
52
|
}
|
|
48
53
|
export declare const api: {
|
|
49
54
|
pages: () => Promise<{
|
|
@@ -51,11 +56,13 @@ export declare const api: {
|
|
|
51
56
|
commit: string;
|
|
52
57
|
pages: RepoPage[];
|
|
53
58
|
}>;
|
|
54
|
-
pageBody: (slug: string) => Promise<{
|
|
59
|
+
pageBody: (slug: string, language?: string) => Promise<{
|
|
55
60
|
ok: true;
|
|
56
61
|
slug: string;
|
|
57
62
|
body: string;
|
|
58
63
|
page: Record<string, unknown>;
|
|
64
|
+
language?: string;
|
|
65
|
+
exists?: boolean;
|
|
59
66
|
}>;
|
|
60
67
|
savePages: (pages: unknown[]) => Promise<{
|
|
61
68
|
ok: true;
|
package/dist/api.js
CHANGED
|
@@ -39,7 +39,7 @@ export const api = {
|
|
|
39
39
|
pages: () => call("/admin/api/pages"),
|
|
40
40
|
/* Il corpo markdown, che nell'elenco non c'è: sono testi lunghi e servono solo alla pagina
|
|
41
41
|
* che si sta aprendo. `page` è il modello letto dallo stesso file. */
|
|
42
|
-
pageBody: (slug) => call(`/admin/api/pages/get?slug=${encodeURIComponent(slug)}`),
|
|
42
|
+
pageBody: (slug, language) => call(`/admin/api/pages/get?slug=${encodeURIComponent(slug)}${language ? `&lang=${encodeURIComponent(language)}` : ""}`),
|
|
43
43
|
/* `shas` è la versione di ogni file DOPO il commit: senza, il salvataggio successivo
|
|
44
44
|
* partirebbe da una versione che non esiste più e verrebbe rifiutato. */
|
|
45
45
|
savePages: (pages) => post("/admin/api/pages", { pages }),
|
package/dist/app/App.js
CHANGED
|
@@ -108,6 +108,11 @@ export function App() {
|
|
|
108
108
|
? list.find((p) => p.slug === route[1])
|
|
109
109
|
: undefined;
|
|
110
110
|
const seoEditing = !!editing && route[2] === "seo";
|
|
111
|
+
const languages = site().languages;
|
|
112
|
+
const [pageLanguage, setPageLanguage] = useState(() => languages?.default ?? "");
|
|
113
|
+
/* Ogni pagina si apre nella sua lingua predefinita. La scelta resta locale alla pagina aperta:
|
|
114
|
+
non deve cambiare Home, Media o la pagina successiva nell'elenco. */
|
|
115
|
+
useEffect(() => { setPageLanguage(languages?.default ?? ""); }, [editing?.slug, languages?.default]);
|
|
111
116
|
/* Le destinazioni interne del dialogo «Collegamento», dalle pagine già caricate: la barra di
|
|
112
117
|
formattazione compare dentro widget annidati anche di tre livelli, e le riceve da un
|
|
113
118
|
contesto invece che da una catena di prop (vedi adminkit form/prose/pages.ts). Le pagine
|
|
@@ -211,7 +216,7 @@ export function App() {
|
|
|
211
216
|
});
|
|
212
217
|
return (_jsx(ProsePages.Provider, { value: pageLinks, children: _jsxs("div", { className: "app" + (hasList(section) && !offModule ? "" : " is-two"), children: [_jsxs("nav", { className: "rail", children: [_jsxs("div", { className: "rail-brand", children: [_jsx("span", { className: "rail-brand-name", children: site().name }), _jsx("span", { className: "rail-brand-role", children: "pannello" })] }), _jsx("div", { className: "rail-nav", children: navGroups.map((g) => (_jsxs("div", { className: "rail-group", children: [g.title && _jsx("span", { className: "rail-group-title", children: g.title }), g.items.map((n) => (_jsxs("button", { className: "rail-item" + (section === n.id ? " is-on" : ""), onClick: () => go(`/${n.id}`), children: [_jsx(n.icon, {}), _jsx("span", { children: n.label }), "badge" in n && n.badge ? _jsx("span", { className: "rail-badge", children: n.badge }) : null] }, n.id)))] }, g.title ?? "top"))) }), _jsxs("div", { className: "rail-foot", children: [_jsxs("a", { className: "rail-item", href: "/", target: "_blank", rel: "noreferrer", children: [_jsx(Icon.out, {}), _jsx("span", { children: "Vedi il sito" })] }), _jsxs("button", { className: "rail-item", onClick: toggleTheme, children: [theme === "dark" ? _jsx(Icon.sun, {}) : _jsx(Icon.moon, {}), _jsx("span", { children: theme === "dark" ? "Tema chiaro" : "Tema scuro" })] }), _jsxs("button", { className: "rail-item" + (section === "settings" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] }), hasList(section) && !offModule && (_jsxs("div", { className: "col", children: [section === "pages" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: list && _jsx(NewPageButton, { pages: list, onCreate: (p) => pages.update([...list, p]) }) }), _jsx("div", { className: "col-body", children: pages.error ? _jsx("p", { className: "ak-error", children: pages.error })
|
|
213
218
|
: !list ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
214
|
-
: _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), onChange: pages.update, onRenamed: (from, to) => {
|
|
219
|
+
: _jsx(PagesList, { pages: list, current: editing?.slug, onEdit: (slug) => go(`/pages/${slug}`), languages: languages, onChange: pages.update, onRenamed: (from, to) => {
|
|
215
220
|
if (editing?.slug === from)
|
|
216
221
|
go(`/pages/${to}${seoEditing ? "/seo" : ""}`);
|
|
217
222
|
} }) })] })), section === "media" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Cartelle" }) }), _jsx("div", { className: "col-body", children: _jsx(MediaFolders, { lib: media, trash: { active: view === "trash", open: () => go("/trash"), leave: () => go("/media") } }) })] })), section === "requests" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: pickedReq.size === 0 ? (_jsx("span", { className: "col-title", children: requests.rows ? `${requests.rows.length} richieste` : "Richieste" })) : (_jsxs("span", { className: "rq-bulk", children: [_jsxs("span", { className: "col-title", children: [pickedReq.size, " scelte"] }), _jsx("button", { className: "mg-act", onClick: () => { void requests.mark([...pickedReq], true); setPickedReq(new Set()); }, children: "Gestite" }), _jsx("button", { className: "mg-act is-danger", onClick: () => { void requests.remove([...pickedReq]); setPickedReq(new Set()); }, children: "Elimina" }), _jsx("button", { className: "mg-act", onClick: () => setPickedReq(new Set()), children: "\u00D7" })] })) }), _jsx("div", { className: "col-body", children: requests.error ? _jsx("p", { className: "ak-error", children: requests.error })
|
|
@@ -234,7 +239,7 @@ export function App() {
|
|
|
234
239
|
: deliveries.rows.length === 0 ? _jsx("p", { className: "ak-hint", children: "Nessun transfer, per ora." })
|
|
235
240
|
: _jsx(DeliveriesList, { rows: deliveries.rows, current: activeDeliveryId || quickSelectedId, onOpen: (id) => { setQuickSelectedId(undefined); go(`/deliveries/${id}`); } }) }), _jsxs("div", { className: "dl-col-foot", children: [deliveries.rows && _jsx(DeliveriesTotal, { rows: deliveries.rows }), _jsxs("button", { className: "col-item" + (deliverySettingsOpen ? " is-on" : ""), onClick: () => { setQuickSelectedId(undefined); go("/deliveries/settings"); }, children: [_jsx(Icon.settings, {}), _jsx("span", { children: "Impostazioni" })] })] })] })), section === "bookings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: bookings.rows ? `${bookings.rows.length} appuntamenti` : "Prenotazioni" }) }), _jsx("div", { className: "col-body", children: bookings.error ? _jsx("p", { className: "ak-error", children: bookings.error })
|
|
236
241
|
: !bookings.rows ? _jsx("p", { className: "ak-hint", children: "Carico\u2026" })
|
|
237
|
-
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`) })) : view === "media" ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && (_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => media.setUploadOpen(true), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
242
|
+
: _jsx(BookingsList, { rows: bookings.rows, current: bookingSettingsOpen ? "settings" : openBooking?.id, onOpen: (id) => go(`/bookings/${id}`), onAgenda: () => go("/bookings"), onSettings: () => go("/bookings/settings") }) })] })), section === "home" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Sezioni" }) }), _jsx("div", { className: "col-body", children: _jsx("ul", { className: "col-list", children: homeGroups.map((g) => (_jsx("li", { children: _jsx("button", { className: "col-item" + (g === homeGroup ? " is-on" : ""), onClick: () => go(`/home/${groupSlug(g)}`), children: g || "Generale" }) }, g))) }) })] })), section === "settings" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "col-head", children: _jsx("span", { className: "col-title", children: "Impostazioni" }) }), _jsx("div", { className: "col-body", children: _jsxs("ul", { className: "col-list settings-list", children: [_jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "site" ? " is-on" : ""), onClick: () => go("/settings/site"), children: [_jsx("strong", { children: "Sito" }), _jsx("span", { children: "Favicon, identit\u00E0 e accesso generale." })] }) }), _jsx("li", { children: _jsxs("button", { className: "col-item settings-list-item" + (settingsView === "seo" ? " is-on" : ""), onClick: () => go("/settings/seo"), children: [_jsx("strong", { children: "SEO" }), _jsx("span", { children: "Motori di ricerca e crawler AI." })] }) })] }) })] }))] })), _jsxs("main", { className: "pane", children: [_jsxs("header", { className: "pane-head", children: [editing ? (_jsx(PageHeader, { page: editing, pages: list ?? [], onChange: replace, seoActive: seoEditing, onSeoToggle: () => go(`/pages/${editing.slug}${seoEditing ? "" : "/seo"}`), languages: languages, activeLanguage: pageLanguage, onLanguageChange: setPageLanguage })) : view === "media" ? (_jsxs("nav", { className: "pane-crumbs", "aria-label": "Percorso", children: [_jsx("button", { className: "pane-crumb" + (mediaCrumbs.length ? "" : " is-current"), onClick: () => media.go(""), children: "Senza cartella" }), mediaCrumbs.map((c, i) => (_jsxs("span", { className: "pane-crumb-part", children: [_jsx("span", { className: "pane-crumb-sep", "aria-hidden": "true", children: "\u203A" }), _jsx("button", { className: "pane-crumb" + (i === mediaCrumbs.length - 1 ? " is-current" : ""), onClick: () => media.go(c.prefix), children: c.label })] }, c.prefix)))] })) : (_jsx("h1", { className: "pane-title", children: heading })), view === "media" && _jsx("span", { className: "pane-count", children: media.shown.length }), _jsx("span", { className: "pane-spacer" }), view === "media" && (_jsx("button", { className: "ak-btn ak-btn-primary", onClick: () => media.setUploadOpen(true), children: "Carica immagini" })), saver?.notice && (_jsx("button", { className: "pane-notice", onClick: () => saver.setNotice(null), children: saver.notice })), section === "bookings" && bookings.notice && (_jsx("button", { className: "pane-notice", onClick: bookings.clearNotice, children: bookings.notice })), publish.notice && (_jsx("button", { className: "pane-notice", onClick: publish.clearNotice, children: publish.notice })), !publish.configured && publish.error && (_jsx("span", { className: "pane-notice", title: publish.error, children: publish.error })), saver && (_jsxs(_Fragment, { children: [saver.dirty && _jsx("span", { className: "pane-dot", title: "modifiche non salvate" }), _jsx("button", { className: "ak-btn", disabled: !saver.dirty || saver.saving,
|
|
238
243
|
/* Dopo un salvataggio il conteggio delle modifiche in attesa è cambiato: senza
|
|
239
244
|
rileggerlo, «Pubblica» comparirebbe solo al ricaricamento della pagina — cioè
|
|
240
245
|
il pannello direbbe che non c'è niente da pubblicare subito dopo aver salvato
|
|
@@ -248,7 +253,7 @@ export function App() {
|
|
|
248
253
|
_jsx(Overview, { deliveries: deliveries.rows, expiring: deliveries.expiring, requests: requests.rows, pending: requests.pending, features: settings.data?.features ?? null, bookings: bookings.rows, bookingsPending: bookings.pending, bookingsError: bookings.error, go: go })), section === "bookings" && (bookings.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: bookings.error }), _jsx("button", { className: "ak-btn", onClick: () => void bookings.load(), children: "Riprova" })] })) : !bookings.rows || !bookings.settings ? _jsx("p", { className: "pane-empty ak-hint", children: "Carico\u2026" })
|
|
249
254
|
: bookingSettingsOpen ? _jsx(BookingSettingsHub, { store: bookings })
|
|
250
255
|
: openBooking ? _jsx(BookingDetail, { row: openBooking, store: bookings })
|
|
251
|
-
: _jsx(BookingAgenda, { store: bookings, onOpen: (id) => go(`/bookings/${id}`) })), section === "pages" && (pages.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: pages.error }), _jsx("button", { className: "ak-btn", onClick: () => void pages.load(), children: "Riprova" })] })) : editing ? (seoEditing ? (_jsx(PageSeoEditor, { page: editing, onChange: replace, onRefresh: pages.refresh, pristine: pages.isPristine(editing.slug), pickMediaFiles: pickMediaFiles, siteSettings: settings.data })) : (_jsx(PageEditor, { page: editing, onChange: replace, onRefresh: pages.refresh, pristine: pages.isPristine(editing.slug), pickMedia: pickMedia, pickMediaFiles: pickMediaFiles }))) : (_jsx("p", { className: "pane-empty ak-hint", children: list ? "Scegli una pagina dall'elenco, o creane una nuova." : "Carico…" }))), section === "home" && (
|
|
256
|
+
: _jsx(BookingAgenda, { store: bookings, onOpen: (id) => go(`/bookings/${id}`) })), section === "pages" && (pages.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: pages.error }), _jsx("button", { className: "ak-btn", onClick: () => void pages.load(), children: "Riprova" })] })) : editing ? (seoEditing ? (_jsx(PageSeoEditor, { page: editing, onChange: replace, onRefresh: pages.refresh, pristine: pages.isPristine(editing.slug), pickMediaFiles: pickMediaFiles, siteSettings: settings.data, readOnly: !!languages && pageLanguage !== languages.default })) : (_jsx(PageEditor, { page: editing, onChange: replace, onRefresh: pages.refresh, pristine: pages.isPristine(editing.slug), pickMedia: pickMedia, pickMediaFiles: pickMediaFiles, activeLanguage: pageLanguage, defaultLanguage: languages?.default }))) : (_jsx("p", { className: "pane-empty ak-hint", children: list ? "Scegli una pagina dall'elenco, o creane una nuova." : "Carico…" }))), section === "home" && (
|
|
252
257
|
/* Con un errore di caricamento c'era solo la riga rossa: nessun modo di riprovare
|
|
253
258
|
se non ricaricando il pannello a mano. Le altre sezioni il bottone ce l'hanno. */
|
|
254
259
|
home.error ? (_jsxs("div", { className: "pane-empty", children: [_jsx("p", { className: "ak-error", children: home.error }), _jsx("button", { className: "ak-btn", onClick: () => void home.load(), children: "Riprova" })] }))
|
package/dist/app/app.css
CHANGED
|
@@ -305,6 +305,13 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
305
305
|
.pg-icon { color: var(--ak-ink-quiet); }
|
|
306
306
|
.pg-off { color: var(--ak-danger); opacity: .7; }
|
|
307
307
|
.pg-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
308
|
+
.pg-language {
|
|
309
|
+
flex: none; min-width: 22px; padding: 3px 4px; border: 1px solid var(--ak-line); border-radius: 3px;
|
|
310
|
+
color: var(--ak-ink-quiet); font: 700 8px/1 var(--ak-font); letter-spacing: .05em; text-align: center;
|
|
311
|
+
}
|
|
312
|
+
.pg-language.is-saved { border-color: color-mix(in srgb, #168666 40%, var(--ak-line)); color: #168666; }
|
|
313
|
+
.pg-language.is-draft { border-color: color-mix(in srgb, var(--ak-accent) 44%, var(--ak-line)); color: var(--ak-accent); }
|
|
314
|
+
.pg-language.is-inherited { opacity: .5; }
|
|
308
315
|
|
|
309
316
|
/* ---- Azioni sulla riga: popover ancorato ------------------------------------------------ */
|
|
310
317
|
.pa { position: relative; flex: none; }
|
|
@@ -348,6 +355,7 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
348
355
|
font: 600 17px/1.2 var(--ak-font); white-space: nowrap; text-align: left;
|
|
349
356
|
}
|
|
350
357
|
.pe-title:hover { color: var(--ak-accent); }
|
|
358
|
+
.pe-title:disabled { cursor: default; color: var(--ak-ink); }
|
|
351
359
|
.pe-title:focus-visible,
|
|
352
360
|
.pe-seo-button:focus-visible,
|
|
353
361
|
.pe-status:focus-visible,
|
|
@@ -366,6 +374,25 @@ body { margin: 0; background: var(--ak-bg); color: var(--ak-ink); font-family: v
|
|
|
366
374
|
color: var(--ak-accent);
|
|
367
375
|
}
|
|
368
376
|
.pe-seo-button .ico { width: 14px; height: 14px; }
|
|
377
|
+
.pe-seo-button:disabled,
|
|
378
|
+
.pe-status:disabled,
|
|
379
|
+
.pe-menu-toggle:has(input:disabled) { opacity: .46; cursor: not-allowed; }
|
|
380
|
+
.pe-language {
|
|
381
|
+
flex: none; display: inline-flex; overflow: hidden; border: 1px solid var(--ak-line); border-radius: 4px;
|
|
382
|
+
background: var(--ak-bg);
|
|
383
|
+
}
|
|
384
|
+
.pe-language button {
|
|
385
|
+
min-width: 33px; min-height: 27px; border: 0; border-left: 1px solid var(--ak-line); padding: 0 8px;
|
|
386
|
+
background: transparent; color: var(--ak-ink-quiet); cursor: pointer; font: 700 10px/1 var(--ak-font); letter-spacing: .06em;
|
|
387
|
+
}
|
|
388
|
+
.pe-language button:first-child { border-left: 0; }
|
|
389
|
+
.pe-language button:hover { color: var(--ak-ink); }
|
|
390
|
+
.pe-language button.is-active { background: var(--ak-panel); color: var(--ak-accent); box-shadow: inset 0 -2px var(--ak-accent); }
|
|
391
|
+
.pe-translation-note {
|
|
392
|
+
margin: 0; padding: 11px 13px; border: 1px solid color-mix(in srgb, var(--ak-accent) 36%, var(--ak-line));
|
|
393
|
+
border-radius: 6px; background: color-mix(in srgb, var(--ak-accent) 6%, var(--ak-panel)); color: var(--ak-ink-quiet);
|
|
394
|
+
font: 400 13px/1.45 var(--ak-font);
|
|
395
|
+
}
|
|
369
396
|
.pe-visibility { position: relative; flex: none; }
|
|
370
397
|
.pe-status {
|
|
371
398
|
display: inline-flex; align-items: center; gap: 7px; border: 0; padding: 5px 2px;
|
package/dist/app/lib/pages.d.ts
CHANGED
|
@@ -4,6 +4,17 @@ export type PageModel = Record<string, unknown> & {
|
|
|
4
4
|
/** Versione del file da cui questa pagina è stata letta. Torna al server nel salvataggio: se
|
|
5
5
|
* nel frattempo il file è cambiato, il server rifiuta invece di sovrascrivere. */
|
|
6
6
|
_sha?: string;
|
|
7
|
+
/** Presenza/versione delle sibling dichiarate; il loro contenuto è caricato solo aprendole. */
|
|
8
|
+
translations?: Record<string, {
|
|
9
|
+
exists: boolean;
|
|
10
|
+
sha?: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** Modelli delle lingue aperte nell'editor, da scrivere nello stesso commit della pagina base. */
|
|
13
|
+
_translations?: Record<string, {
|
|
14
|
+
page: Record<string, unknown>;
|
|
15
|
+
body?: string;
|
|
16
|
+
_sha?: string;
|
|
17
|
+
}>;
|
|
7
18
|
};
|
|
8
19
|
export declare function usePages(): {
|
|
9
20
|
pages: PageModel[] | null;
|
package/dist/app/lib/pages.js
CHANGED
|
@@ -31,7 +31,7 @@ export function usePages() {
|
|
|
31
31
|
* andava riconciliato con `modelFromRaw` — un passaggio in meno, e una grafia in meno da
|
|
32
32
|
* far combaciare. L'ordine per peso lo dà già il server. */
|
|
33
33
|
const list = (d.pages ?? []).map((p) => ({
|
|
34
|
-
...p.page, slug: p.slug, url: p.url, _orig: p.slug, _sha: p.sha,
|
|
34
|
+
...p.page, slug: p.slug, url: p.url, _orig: p.slug, _sha: p.sha, translations: p.translations,
|
|
35
35
|
}));
|
|
36
36
|
base.current = new Map(list.map((p) => [p.slug, p]));
|
|
37
37
|
setPages(list);
|
|
@@ -91,6 +91,22 @@ export function usePages() {
|
|
|
91
91
|
const s = r.shas?.[p.slug];
|
|
92
92
|
if (s)
|
|
93
93
|
p._sha = s;
|
|
94
|
+
/* Le sibling hanno lo stesso ciclo di vita del file base: se non si aggiorna il loro
|
|
95
|
+
SHA qui, la modifica EN appena salvata parte già da una versione che non esiste più
|
|
96
|
+
e il Worker la rifiuta come conflitto al click successivo. */
|
|
97
|
+
for (const code of site().languages?.available ?? []) {
|
|
98
|
+
if (code === site().languages?.default)
|
|
99
|
+
continue;
|
|
100
|
+
const translationSha = r.shas?.[`${p.slug}.${code}`];
|
|
101
|
+
if (!translationSha)
|
|
102
|
+
continue;
|
|
103
|
+
p.translations = {
|
|
104
|
+
...(p.translations ?? {}),
|
|
105
|
+
[code]: { exists: true, sha: translationSha },
|
|
106
|
+
};
|
|
107
|
+
if (p._translations?.[code])
|
|
108
|
+
p._translations[code]._sha = translationSha;
|
|
109
|
+
}
|
|
94
110
|
});
|
|
95
111
|
// Da qui in poi il file contiene ciò che hanno queste pagine: tornano tutte "non toccate".
|
|
96
112
|
base.current = new Map(pages.map((p) => [p.slug, p]));
|
|
@@ -14,7 +14,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
* scomparire mentre la si rinomina fa perdere il segno di dove si era.
|
|
15
15
|
*/
|
|
16
16
|
import { useMemo, useState } from "react";
|
|
17
|
-
import { ui } from "../../index.js";
|
|
18
17
|
import { site } from "../site.js";
|
|
19
18
|
import { slugify, uniqueSlug } from "../lib/slug";
|
|
20
19
|
import { Icon } from "../ui/icons";
|
|
@@ -33,7 +32,7 @@ import { Icon } from "../ui/icons";
|
|
|
33
32
|
* modulo andava bene: è il genere di cosa che cambia quando una dipendenza smette di essere
|
|
34
33
|
* statica e diventa iniettata. */
|
|
35
34
|
function tipiDisponibili() {
|
|
36
|
-
const tipi =
|
|
35
|
+
const tipi = Object.entries(site().pages).map(([value, page]) => ({ value, label: page.label }));
|
|
37
36
|
/* La preselezione: "Pagina" se c'è (è la scelta di gran lunga più frequente), altrimenti la
|
|
38
37
|
* prima opzione — così questo elenco non può restare senza un valore valido. */
|
|
39
38
|
const iniziale = tipi.some((t) => t.value === "page") ? "page" : (tipi[0]?.value ?? "page");
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import type { AssetFile } from "../../index.js";
|
|
2
2
|
import type { PageModel } from "../lib/pages";
|
|
3
|
-
export declare function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, }: {
|
|
3
|
+
export declare function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, languages, activeLanguage, onLanguageChange, }: {
|
|
4
4
|
page: PageModel;
|
|
5
5
|
pages: PageModel[];
|
|
6
6
|
onChange: (next: PageModel) => void;
|
|
7
7
|
seoActive: boolean;
|
|
8
8
|
onSeoToggle: () => void;
|
|
9
|
+
/** Assente nei siti monolingui: il controllo non viene nemmeno disegnato. */
|
|
10
|
+
languages?: {
|
|
11
|
+
default: string;
|
|
12
|
+
available: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
activeLanguage?: string;
|
|
15
|
+
onLanguageChange?: (language: string) => void;
|
|
9
16
|
}): import("react").JSX.Element;
|
|
10
|
-
export declare function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, }: {
|
|
17
|
+
export declare function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }: {
|
|
11
18
|
page: PageModel;
|
|
12
19
|
onChange: (next: PageModel) => void;
|
|
13
20
|
/** Sostituisce la pagina con quella letta dal repo, senza contare come modifica dell'utente. */
|
|
@@ -20,4 +27,7 @@ export declare function PageEditor({ page, onChange, onRefresh, pristine, pickMe
|
|
|
20
27
|
pickMediaFiles?: (o: {
|
|
21
28
|
multiple: boolean;
|
|
22
29
|
}) => Promise<AssetFile[] | null>;
|
|
30
|
+
/** La lingua è scelta nella testata della pagina, non è un'impostazione globale del pannello. */
|
|
31
|
+
activeLanguage?: string;
|
|
32
|
+
defaultLanguage?: string;
|
|
23
33
|
}): import("react").JSX.Element;
|
|
@@ -14,7 +14,7 @@ import { SortableContext, arrayMove, rectSortingStrategy, sortableKeyboardCoordi
|
|
|
14
14
|
import { CSS } from "@dnd-kit/utilities";
|
|
15
15
|
import { ProseField, SchemaForm } from "../../form/index.js";
|
|
16
16
|
import { api } from "../../index.js";
|
|
17
|
-
import {
|
|
17
|
+
import { pageType } from "../site.js";
|
|
18
18
|
import { uniqueSlug } from "../lib/slug";
|
|
19
19
|
import { proseStats } from "../lib/text";
|
|
20
20
|
import { Icon } from "../ui/icons";
|
|
@@ -22,7 +22,7 @@ import { GallerySectionsEditor } from "./GallerySectionsEditor";
|
|
|
22
22
|
import { PortfolioWorksEditor } from "./PortfolioWorksEditor";
|
|
23
23
|
import { AboutEditor } from "./AboutEditor";
|
|
24
24
|
import { WorkshopEditionsEditor } from "./WorkshopEditionsEditor";
|
|
25
|
-
export function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, }) {
|
|
25
|
+
export function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, languages, activeLanguage, onLanguageChange, }) {
|
|
26
26
|
const [titleOpen, setTitleOpen] = useState(false);
|
|
27
27
|
const [visibilityOpen, setVisibilityOpen] = useState(false);
|
|
28
28
|
const [title, setTitle] = useState(String(page.title ?? ""));
|
|
@@ -69,12 +69,13 @@ export function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, }) {
|
|
|
69
69
|
setTitleOpen(false);
|
|
70
70
|
};
|
|
71
71
|
const online = page.published !== false;
|
|
72
|
-
|
|
72
|
+
const translationActive = !!languages && activeLanguage !== languages.default;
|
|
73
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "pe-head-main", children: [_jsx("button", { type: "button", className: "pe-title", title: "Modifica nome e indirizzo", disabled: translationActive, onClick: () => {
|
|
73
74
|
setTitle(String(page.title ?? ""));
|
|
74
75
|
setSlug(page.slug);
|
|
75
76
|
setVisibilityOpen(false);
|
|
76
77
|
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
|
+
}, children: String(page.title ?? page.slug) }), languages && (_jsx("div", { className: "pe-language", "aria-label": "Lingua del contenuto", children: languages.available.map((language) => (_jsx("button", { type: "button", className: activeLanguage === language ? "is-active" : "", "aria-pressed": activeLanguage === language, onClick: () => onLanguageChange?.(language), children: language.toUpperCase() }, language))) })), _jsxs("button", { type: "button", className: "pe-seo-button" + (seoActive ? " is-active" : ""), "aria-pressed": seoActive, disabled: translationActive, 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, disabled: translationActive, 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
79
|
if (!online)
|
|
79
80
|
onChange({ ...page, published: true });
|
|
80
81
|
setVisibilityOpen(false);
|
|
@@ -82,7 +83,7 @@ export function PageHeader({ page, pages, onChange, seoActive, onSeoToggle, }) {
|
|
|
82
83
|
if (online)
|
|
83
84
|
onChange({ ...page, published: false });
|
|
84
85
|
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
|
+
}, 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, disabled: translationActive, 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
87
|
if (event.target === event.currentTarget)
|
|
87
88
|
setTitleOpen(false);
|
|
88
89
|
}, 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) => {
|
|
@@ -161,15 +162,51 @@ function GalleryEditor({ value, onChange, pickMediaFiles, }) {
|
|
|
161
162
|
? { ...candidate, hidden: candidate.hidden ? undefined : true }
|
|
162
163
|
: 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
|
}
|
|
164
|
-
export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, }) {
|
|
165
|
+
export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pickMediaFiles, activeLanguage, defaultLanguage, }) {
|
|
165
166
|
const [bodyState, setBodyState] = useState("idle");
|
|
166
|
-
const
|
|
167
|
+
const translationActive = !!defaultLanguage && !!activeLanguage && activeLanguage !== defaultLanguage;
|
|
168
|
+
const translationKey = translationActive ? `${page.slug}:${activeLanguage}` : "";
|
|
169
|
+
/* Aprire una lingua non è una modifica: finché non si cambia davvero un campo, il modello
|
|
170
|
+
composto resta qui e il pulsante Salva non si accende. */
|
|
171
|
+
const [openedTranslations, setOpenedTranslations] = useState({});
|
|
172
|
+
const translation = translationActive
|
|
173
|
+
? page._translations?.[activeLanguage] ?? openedTranslations[translationKey]
|
|
174
|
+
: undefined;
|
|
175
|
+
const [translationState, setTranslationState] = useState("idle");
|
|
176
|
+
const definition = pageType(page.type);
|
|
177
|
+
const showBody = definition?.hasBody === true;
|
|
167
178
|
const isGallery = page.type === "gallery";
|
|
168
179
|
const isPortfolio = page.type === "portfolio";
|
|
180
|
+
/* Il Worker restituisce già il modello composto: l'editor può mostrarlo come una pagina
|
|
181
|
+
ordinaria. Aprirlo resta una lettura; il primo cambio lo sposta in `_translations`,
|
|
182
|
+
lasciando immutata la pagina predefinita e rendendo esplicita la futura scrittura. */
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!translationActive || translation || !activeLanguage)
|
|
185
|
+
return;
|
|
186
|
+
let alive = true;
|
|
187
|
+
setTranslationState("loading");
|
|
188
|
+
api.pageBody(page.slug, activeLanguage)
|
|
189
|
+
.then((data) => {
|
|
190
|
+
if (!alive)
|
|
191
|
+
return;
|
|
192
|
+
setTranslationState("idle");
|
|
193
|
+
setOpenedTranslations((current) => ({
|
|
194
|
+
...current,
|
|
195
|
+
[translationKey]: {
|
|
196
|
+
page: data.page,
|
|
197
|
+
...(showBody ? { body: data.body } : {}),
|
|
198
|
+
_sha: page.translations?.[activeLanguage]?.sha,
|
|
199
|
+
},
|
|
200
|
+
}));
|
|
201
|
+
})
|
|
202
|
+
.catch(() => { if (alive)
|
|
203
|
+
setTranslationState("error"); });
|
|
204
|
+
return () => { alive = false; };
|
|
205
|
+
}, [translationActive, translation, activeLanguage, page.slug, page.translations, showBody, translationKey]);
|
|
169
206
|
/* APRENDO UNA PAGINA SI RILEGGE DAL REPO, non dal manifest pubblicato. */
|
|
170
207
|
const [loadedFor, setLoadedFor] = useState(null);
|
|
171
208
|
useEffect(() => {
|
|
172
|
-
if (loadedFor === page.slug || !pristine)
|
|
209
|
+
if (translationActive || loadedFor === page.slug || !pristine)
|
|
173
210
|
return;
|
|
174
211
|
let alive = true;
|
|
175
212
|
if (showBody)
|
|
@@ -195,7 +232,36 @@ export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pic
|
|
|
195
232
|
}
|
|
196
233
|
});
|
|
197
234
|
return () => { alive = false; };
|
|
198
|
-
}, [page.slug, showBody, pristine, loadedFor]);
|
|
235
|
+
}, [page.slug, showBody, pristine, loadedFor, translationActive]);
|
|
236
|
+
if (translationActive && translationState === "loading" && !translation) {
|
|
237
|
+
return _jsx("p", { className: "pane-empty ak-hint", children: "Carico la traduzione\u2026" });
|
|
238
|
+
}
|
|
239
|
+
if (translationActive && translationState === "error" && !translation) {
|
|
240
|
+
return _jsx("p", { className: "pe-load-error", children: "Non sono riuscito a caricare la traduzione. Riprova o torna alla lingua predefinita." });
|
|
241
|
+
}
|
|
242
|
+
const shownPage = translationActive && translation
|
|
243
|
+
? { ...page, ...translation.page, ...(showBody ? { body: translation.body ?? "" } : {}) }
|
|
244
|
+
: page;
|
|
245
|
+
const changeShown = (next) => {
|
|
246
|
+
if (!translationActive || !activeLanguage) {
|
|
247
|
+
onChange(next);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const { _translations: _ignoredTranslations, translations: _ignoredPresence, _orig: _ignoredOrig, _sha: _ignoredSha, url: _ignoredUrl, body, ...model } = next;
|
|
251
|
+
const update = {
|
|
252
|
+
page: model,
|
|
253
|
+
...(showBody ? { body: typeof body === "string" ? body : "" } : {}),
|
|
254
|
+
_sha: translation?._sha ?? page.translations?.[activeLanguage]?.sha,
|
|
255
|
+
};
|
|
256
|
+
setOpenedTranslations((current) => ({ ...current, [translationKey]: update }));
|
|
257
|
+
onChange({
|
|
258
|
+
...page,
|
|
259
|
+
_translations: {
|
|
260
|
+
...(page._translations ?? {}),
|
|
261
|
+
[activeLanguage]: update,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
};
|
|
199
265
|
const commonOmissions = useMemo(() => [
|
|
200
266
|
"title", "published", "inMenu",
|
|
201
267
|
"seoTitle", "seoDescription", "ogTitle", "ogDescription", "ogImage",
|
|
@@ -205,29 +271,31 @@ export function PageEditor({ page, onChange, onRefresh, pristine, pickMedia, pic
|
|
|
205
271
|
«Ritratto» e «Riconoscimenti» — campi di altri tipi di pagina, che compaiono perché il
|
|
206
272
|
form disegna tutto ciò che lo schema dichiara e non sa quale tipo si stia modificando.
|
|
207
273
|
Titolo, visibilità e SEO restano dove sono già: la barra in alto e la scheda SEO. */
|
|
208
|
-
if (isPortfolio) {
|
|
274
|
+
if (isPortfolio && !translationActive) {
|
|
209
275
|
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
276
|
}
|
|
211
277
|
/* Stessa ragione delle due qui sopra: la pagina "chi sono" ha un'anatomia sua — biografia,
|
|
212
278
|
ritratto, riconoscimenti — e il form generico ci metteva accanto «Lavori», «Edizioni» e
|
|
213
279
|
«Sezioni», cioè i campi delle altre tipologie. Il corpo markdown è la biografia, quindi la
|
|
214
280
|
textarea in fondo non serve più: sta dentro l'editor, in cima, dov'è il suo posto. */
|
|
215
|
-
if (page.type === "about") {
|
|
281
|
+
if (page.type === "about" && !translationActive) {
|
|
216
282
|
return (_jsx("div", { className: "ed ed-about", children: _jsx(AboutEditor, { page: page, onChange: onChange, bodyState: bodyState, pickMediaFiles: pickMediaFiles }) }));
|
|
217
283
|
}
|
|
218
284
|
/* La pagina workshop: l'introduzione e le edizioni, nell'ordine in cui escono sul sito —
|
|
219
285
|
l'introduzione apre la colonna dei testi, sopra la prima locandina (WORKSHOP.md). Il corpo
|
|
220
286
|
markdown è quell'introduzione, quindi non c'è nessuna textarea in fondo: sta in cima, dove
|
|
221
287
|
la si legge. */
|
|
222
|
-
if (page.type === "workshop") {
|
|
288
|
+
if (page.type === "workshop" && !translationActive) {
|
|
223
289
|
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
290
|
}
|
|
225
|
-
if (isGallery) {
|
|
291
|
+
if (isGallery && !translationActive) {
|
|
226
292
|
/* Le foto si modificano DOVE stanno: dentro il loro blocco, che è anche dove vivono nei dati
|
|
227
293
|
(`sections[].images`) e dove la griglia esce sul sito. Non c'è più un pannello staccato in
|
|
228
294
|
fondo — c'era finché le foto stavano in un campo della pagina, e insieme a lui è sparito
|
|
229
295
|
il caso in cui la stessa griglia si vedeva due volte. */
|
|
230
296
|
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
297
|
}
|
|
232
|
-
return (_jsxs("div", { className: "ed", children: [
|
|
298
|
+
return (_jsxs("div", { className: "ed", children: [translationActive && (_jsx("p", { className: "pe-translation-note", children: page.translations?.[activeLanguage]?.exists || page._translations?.[activeLanguage]
|
|
299
|
+
? `Stai modificando ${activeLanguage.toUpperCase()}. I campi non traducibili restano visibili e vengono copiati dalla lingua predefinita al salvataggio.`
|
|
300
|
+
: `${activeLanguage.toUpperCase()} è uguale all’italiano finché non modifichi e salvi una traduzione.` })), _jsx(SchemaForm, { shape: definition?.shape ?? {}, value: shownPage, omit: commonOmissions, onChange: (next) => changeShown({ ...next, slug: page.slug }), pickMedia: pickMedia, pickMediaFiles: pickMediaFiles, translationReadOnly: translationActive }), 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 shownPage.body === "string" ? shownPage.body : "", onChange: (event) => changeShown({ ...shownPage, body: event.target.value }) })] }))] }));
|
|
233
301
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AssetFile } from "../../index.js";
|
|
2
2
|
import type { PageModel } from "../lib/pages";
|
|
3
3
|
import type { SiteSettings } from "../lib/settings";
|
|
4
|
-
export declare function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFiles, siteSettings, }: {
|
|
4
|
+
export declare function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFiles, siteSettings, readOnly, }: {
|
|
5
5
|
page: PageModel;
|
|
6
6
|
onChange: (next: PageModel) => void;
|
|
7
7
|
onRefresh: (next: PageModel) => void;
|
|
@@ -10,4 +10,7 @@ export declare function PageSeoEditor({ page, onChange, onRefresh, pristine, pic
|
|
|
10
10
|
multiple: boolean;
|
|
11
11
|
}) => Promise<AssetFile[] | null>;
|
|
12
12
|
siteSettings?: SiteSettings | null;
|
|
13
|
+
/** La vista SEO non carica un modello tradotto: in una lingua secondaria non deve mai
|
|
14
|
+
scrivere per sbaglio i metadati della pagina predefinita. */
|
|
15
|
+
readOnly?: boolean;
|
|
13
16
|
}): import("react").JSX.Element;
|
|
@@ -48,7 +48,7 @@ function FieldHelp({ children, }) {
|
|
|
48
48
|
function Count({ value, recommended, max }) {
|
|
49
49
|
return (_jsxs("span", { className: "seo-count" + (value.length > recommended ? " is-long" : ""), children: [value.length, "/", max, " caratteri"] }));
|
|
50
50
|
}
|
|
51
|
-
export function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFiles, siteSettings, }) {
|
|
51
|
+
export function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFiles, siteSettings, readOnly = false, }) {
|
|
52
52
|
const [loadedFor, setLoadedFor] = useState(null);
|
|
53
53
|
const [loadError, setLoadError] = useState(false);
|
|
54
54
|
/* Arrivando direttamente a /seo la pagina normale non viene montata, quindi la lettura dal
|
|
@@ -98,5 +98,5 @@ export function PageSeoEditor({ page, onChange, onRefresh, pristine, pickMediaFi
|
|
|
98
98
|
if (selected?.[0])
|
|
99
99
|
onChange({ ...page, ogImage: selected[0].url });
|
|
100
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." })] })] })] }));
|
|
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." })), readOnly && (_jsx("p", { className: "pe-translation-note", children: "I metadati SEO di questa lingua sono ereditati. Torna alla lingua predefinita per modificarli." })), _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, disabled: readOnly, 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, disabled: readOnly, 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, disabled: readOnly, 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, disabled: readOnly, 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", disabled: readOnly, onClick: () => void chooseImage(), children: explicitImage ? "Cambia immagine" : "Scegli immagine" }), explicitImage && (_jsx("button", { type: "button", className: "ak-btn ak-btn-quiet", disabled: readOnly, 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
102
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PageModel } from "../lib/pages";
|
|
2
|
-
export declare function PagesList({ pages, current, onEdit, onChange, onRenamed, }: {
|
|
2
|
+
export declare function PagesList({ pages, current, onEdit, onChange, onRenamed, languages, }: {
|
|
3
3
|
pages: PageModel[];
|
|
4
4
|
current?: string;
|
|
5
5
|
onEdit: (slug: string) => void;
|
|
@@ -7,4 +7,8 @@ export declare function PagesList({ pages, current, onEdit, onChange, onRenamed,
|
|
|
7
7
|
/** Una rinomina cambia l'indirizzo: se la pagina è quella aperta nell'editor, la rotta punta
|
|
8
8
|
* a uno slug che non esiste più e l'editor resterebbe vuoto. Lo sa solo chi tiene la rotta. */
|
|
9
9
|
onRenamed?: (from: string, to: string) => void;
|
|
10
|
+
languages?: {
|
|
11
|
+
default: string;
|
|
12
|
+
available: readonly string[];
|
|
13
|
+
};
|
|
10
14
|
}): import("react").JSX.Element;
|
|
@@ -20,12 +20,18 @@ import { Icon } from "../ui/icons";
|
|
|
20
20
|
const ICONS = {
|
|
21
21
|
gallery: Icon.gallery, page: Icon.doc, contact: Icon.mail,
|
|
22
22
|
};
|
|
23
|
-
function Row({ p, pages, current, onEdit, onChange, onRemove, onDuplicate }) {
|
|
23
|
+
function Row({ p, pages, current, onEdit, onChange, onRemove, onDuplicate, languages }) {
|
|
24
24
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: p.slug });
|
|
25
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) }),
|
|
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) }), languages?.available.filter((language) => language !== languages.default).map((language) => {
|
|
27
|
+
const saved = p.translations?.[language]?.exists;
|
|
28
|
+
const draft = !!p._translations?.[language];
|
|
29
|
+
return (_jsx("span", { className: "pg-language" + (saved ? " is-saved" : draft ? " is-draft" : " is-inherited"), title: saved ? `${language.toUpperCase()} tradotta` : draft
|
|
30
|
+
? `${language.toUpperCase()} modificata: salva per creare la traduzione`
|
|
31
|
+
: `${language.toUpperCase()} uguale alla lingua predefinita`, children: language.toUpperCase() }, language));
|
|
32
|
+
}), 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
33
|
}
|
|
28
|
-
function Group({ title, items, all, current, onEdit, onReorder, onChange, onRemove, onDuplicate, }) {
|
|
34
|
+
function Group({ title, items, all, current, onEdit, onReorder, onChange, onRemove, onDuplicate, languages, }) {
|
|
29
35
|
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 4 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
30
36
|
const onDragEnd = (e) => {
|
|
31
37
|
const { active, over } = e;
|
|
@@ -33,9 +39,9 @@ function Group({ title, items, all, current, onEdit, onReorder, onChange, onRemo
|
|
|
33
39
|
return;
|
|
34
40
|
onReorder(items.findIndex((p) => p.slug === active.id), items.findIndex((p) => p.slug === over.id));
|
|
35
41
|
};
|
|
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))) }) }) })] }));
|
|
42
|
+
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, languages: languages, onRemove: () => onRemove(p.slug), onDuplicate: () => onDuplicate(p.slug) }, p.slug))) }) }) })] }));
|
|
37
43
|
}
|
|
38
|
-
export function PagesList({ pages, current, onEdit, onChange, onRenamed, }) {
|
|
44
|
+
export function PagesList({ pages, current, onEdit, onChange, onRenamed, languages, }) {
|
|
39
45
|
const inMenu = pages.filter((p) => !!p.inMenu);
|
|
40
46
|
const notInMenu = pages.filter((p) => !p.inMenu);
|
|
41
47
|
const reorder = (group, from, to) => {
|
|
@@ -71,6 +77,6 @@ export function PagesList({ pages, current, onEdit, onChange, onRenamed, }) {
|
|
|
71
77
|
const i = pages.indexOf(src);
|
|
72
78
|
onChange([...pages.slice(0, i + 1), copia, ...pages.slice(i + 1)]);
|
|
73
79
|
};
|
|
74
|
-
const shared = { all: pages, current, onEdit, onChange: replace, onRemove: remove, onDuplicate: duplicate };
|
|
80
|
+
const shared = { all: pages, current, onEdit, onChange: replace, onRemove: remove, onDuplicate: duplicate, languages };
|
|
75
81
|
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
82
|
}
|