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() {
-