From 47008a7ca160d2fbcf44049fc1b98b8dae15d2cb Mon Sep 17 00:00:00 2001 From: pollutri Date: Wed, 27 May 2026 17:45:23 +0200 Subject: [PATCH] Implementate: backup, edit del welcome text, card kiosk-mode --- app/admin/page.tsx | 162 ++++++++++++++++++++++++++++++--- app/api/admin/backup/route.ts | 59 ++++++++++++ app/api/admin/restore/route.ts | 112 +++++++++++++++++++++++ app/api/portals/route.ts | 8 +- app/page.tsx | 10 +- components/FullscreenLock.tsx | 36 ++++++++ components/HeroBanner.tsx | 9 +- lib/sanitize.ts | 12 +++ lib/system-bins.ts | 19 ++++ lib/validation.ts | 1 + types/index.ts | 2 +- 11 files changed, 413 insertions(+), 17 deletions(-) create mode 100644 app/api/admin/backup/route.ts create mode 100644 app/api/admin/restore/route.ts create mode 100644 components/FullscreenLock.tsx create mode 100644 lib/system-bins.ts diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 2b36bcc..735e8b0 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -17,6 +17,63 @@ function CharCounter({ value, limit }: CharCounterProps) { ); } +function stripTags(html: string): string { + if (typeof window === 'undefined' || !html) return ''; + return new DOMParser().parseFromString(html, 'text/html').body.textContent ?? ''; +} + +type RichTextMiniProps = { + value: string; + onChange: (html: string) => void; + limit: number; + className?: string; +}; +function RichTextMini({ value, onChange, limit, className }: RichTextMiniProps) { + const ref = useRef(null); + + // Sync iniziale soltanto. Aggiornare innerHTML durante l'editing perderebbe la + // posizione del cursore, quindi confidiamo che onInput tenga value e DOM allineati. + useEffect(() => { + if (ref.current && ref.current.innerHTML !== value) { + ref.current.innerHTML = value || ''; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const exec = (cmd: 'bold' | 'italic') => { + ref.current?.focus(); + document.execCommand(cmd); + onChange(ref.current?.innerHTML || ''); + }; + + return ( +
+
+ + +
+
onChange((e.target as HTMLDivElement).innerHTML)} + className={className ?? 'w-full border border-gray-300 rounded-lg p-2.5 min-h-[8rem] bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500'} + /> + +
+ ); +} + function StyledSelect({ value, onChange, @@ -519,6 +576,36 @@ export default function AdminDashboard() { showToast('Portal settings saved successfully!'); // Replaced window.alert }; + const handleBackupDownload = () => { + window.location.href = '/api/admin/backup'; + }; + + const [restoring, setRestoring] = useState(false); + const handleRestoreUpload = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + e.target.value = ''; + if (!file) return; + if (!window.confirm('Il ripristino sovrascriverà tutti i dati attuali (card, portale, media, font). Continuare?')) return; + + setRestoring(true); + try { + const fd = new FormData(); + fd.append('file', file); + const res = await fetch('/api/admin/restore', { method: 'POST', body: fd }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + showToast(data?.error || `Errore ripristino (${res.status})`, 'error'); + return; + } + showToast(`Ripristino completato: ${data.restored?.cards ?? 0} card, ${data.restored?.portals ?? 0} portali. Ricarico…`); + setTimeout(() => window.location.reload(), 1200); + } catch (err) { + showToast(`Errore di rete: ${(err as Error).message}`, 'error'); + } finally { + setRestoring(false); + } + }; + // Shared Input Classes for high contrast const inputClasses = "w-full border border-gray-300 p-2.5 rounded-lg outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900 placeholder-gray-400"; @@ -582,6 +669,9 @@ export default function AdminDashboard() { {card.extraMedia && card.extraMedia.length > 0 && ( [{card.extraMedia.length}] )} + {card.cardType === 'FULLSCREEN_LOCK' && ( + LOCK ATTIVA + )}
@@ -615,8 +705,11 @@ export default function AdminDashboard() {
-