import { useState, useEffect, useRef } from 'react'; // ============================================================ // LIBRO PER SORRIDERE — Sistema ordini scuole // ============================================================ const FONT_LINK_ID = 'lps-fonts'; function useFonts() { useEffect(() => { if (document.getElementById(FONT_LINK_ID)) return; const link = document.createElement('link'); link.id = FONT_LINK_ID; link.rel = 'stylesheet'; link.href = 'https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap'; document.head.appendChild(link); }, []); } // ---------- Storage helpers (localStorage) ---------- function useStorageReady() { return true; } const memoryStore = {}; async function safeGet(key) { try { const val = localStorage.getItem('lps_' + key); return val ? JSON.parse(val) : null; } catch (e) { return memoryStore[key] ?? null; } } async function safeSet(key, value) { memoryStore[key] = value; try { localStorage.setItem('lps_' + key, JSON.stringify(value)); return true; } catch (e) { console.error('storage set failed', e); return false; } } // ---------- Demo seed data ---------- const DEMO_LIBRI = [ { id: 'l1', titolo: 'Il mio primo libro di Magia', descrizione: 'Trucchi facili e divertenti per diventare un vero mago.', pagine: 96, prezzo: 8, copertina: '🎩', scorte: 25, venduti: 0, fasceEta: ['primaria_media'] }, { id: 'l2', titolo: "I misteri dell'antico Egitto", descrizione: 'Dei, regine, miti e meraviglie della civiltà egizia.', pagine: 64, prezzo: 8, copertina: '🏺', scorte: 20, venduti: 0, fasceEta: ['primaria_media'] }, { id: 'l3', titolo: 'Barzellette e indovinelli', descrizione: 'Per divertirsi ovunque: a casa, a scuola, in viaggio.', pagine: 64, prezzo: 8, copertina: '😂', scorte: 30, venduti: 0, fasceEta: ['infanzia', 'primaria_media'] }, { id: 'l4', titolo: 'Corpo Umano', descrizione: 'Dalla testa ai piedi, i segreti del corpo umano.', pagine: 64, prezzo: 7, copertina: '🫀', scorte: 20, venduti: 0, fasceEta: ['primaria_media'] }, { id: 'l5', titolo: 'Zanna Bianca', descrizione: 'Il classico di Jack London, ghiacci e avventura.', pagine: null, prezzo: 7, copertina: '🐺', scorte: 15, venduti: 0, fasceEta: ['primaria_media'] }, { id: 'l6', titolo: 'Sudoku Facili', descrizione: 'Oltre 100 schemi facili per allenare la logica.', pagine: 96, prezzo: 8, copertina: '🔢', scorte: 20, venduti: 0, fasceEta: ['primaria_media'] }, ]; const DEMO_VOLANTINI = [ { id: 'v1', nome: 'Volantino Primarie — Settembre', libriIds: ['l1', 'l2', 'l3', 'l4'] }, { id: 'v2', nome: 'Volantino Medie — Settembre', libriIds: ['l3', 'l5', 'l6'] }, ]; const DEMO_ISTITUTI = [ { id: 'ic-ilariaalpi', nome: 'Istituto Comprensivo di Sarzana — Ilaria Alpi', comune: 'Sarzana', via: 'Piazza Dino Ricchetti', cap: '19038', plessi: [ { nome: 'Infanzia Marinella', tipo: 'infanzia' }, { nome: 'Infanzia Sarzanello', tipo: 'infanzia' }, { nome: 'Infanzia San Lazzaro', tipo: 'infanzia' }, { nome: 'Infanzia Sarzana Capoluogo', tipo: 'infanzia' }, { nome: 'Infanzia Lidia Lalli', tipo: 'infanzia' }, { nome: 'Primaria Nave', tipo: 'primaria_media' }, { nome: 'Primaria Marinella', tipo: 'primaria_media' }, { nome: 'Primaria Sarzana Capoluogo', tipo: 'primaria_media' }, { nome: 'Primaria San Lazzaro', tipo: 'primaria_media' }, { nome: 'Primaria Santa Caterina', tipo: 'primaria_media' }, { nome: 'Primaria Ghiaia', tipo: 'primaria_media' }, { nome: 'Primaria Bradia', tipo: 'primaria_media' }, { nome: 'Secondaria Poggi/Carducci', tipo: 'primaria_media' }, ], volantinoId: 'v1', attivo: true, }, { id: 'ic-n7laspezia', nome: 'Istituto Comprensivo N. 7 — La Spezia', comune: 'La Spezia', via: 'Via del Canaletto 165', cap: '19100', plessi: [ { nome: 'Infanzia Canaletto', tipo: 'infanzia' }, { nome: 'Infanzia Fossamastra', tipo: 'infanzia' }, { nome: 'Primaria Pitelli', tipo: 'primaria_media' }, { nome: 'Primaria D. Alighieri', tipo: 'primaria_media' }, { nome: 'Primaria Canaletto - Carducci', tipo: 'primaria_media' }, { nome: 'Secondaria Fontana - Formentini', tipo: 'primaria_media' }, ], volantinoId: 'v2', attivo: true, }, ]; const DEMO_PAYMENT_LINKS = { nexi: '', paypal: '' }; const ADMIN_CODE = '1234'; // ---------- Root ---------- function App() { useFonts(); const storageReady = useStorageReady(); const [view, setView] = useState('parent'); const [istituti, setIstituti] = useState(null); const [libri, setLibri] = useState(null); const [volantini, setVolantini] = useState(null); const [ordini, setOrdini] = useState(null); const [paymentLinks, setPaymentLinks] = useState(null); const [loaded, setLoaded] = useState(false); useEffect(() => { if (!storageReady) return; (async () => { const [i, l, v, o, p] = await Promise.all([ safeGet('istituti'), safeGet('libri'), safeGet('volantini'), safeGet('ordini'), safeGet('paymentLinks'), ]); setIstituti(i ?? DEMO_ISTITUTI); setLibri(l ?? DEMO_LIBRI); setVolantini(v ?? DEMO_VOLANTINI); setOrdini(o ?? []); setPaymentLinks(p ?? DEMO_PAYMENT_LINKS); setLoaded(true); if (!i) await safeSet('istituti', DEMO_ISTITUTI); if (!l) await safeSet('libri', DEMO_LIBRI); if (!v) await safeSet('volantini', DEMO_VOLANTINI); if (!o) await safeSet('ordini', []); if (!p) await safeSet('paymentLinks', DEMO_PAYMENT_LINKS); })(); }, [storageReady]); async function persistIstituti(next) { setIstituti(next); await safeSet('istituti', next); } async function persistLibri(next) { setLibri(next); await safeSet('libri', next); } async function persistVolantini(next) { setVolantini(next); await safeSet('volantini', next); } async function persistOrdini(next) { setOrdini(next); await safeSet('ordini', next); } async function persistPaymentLinks(next) { setPaymentLinks(next); await safeSet('paymentLinks', next); } if (!loaded) { return (
📖
Apertura del catalogo…
); } return (
{view === 'parent' ? ( ) : ( )}
); } // ---------- Top switch ---------- function TopSwitch({ view, setView }) { const [showAdminGate, setShowAdminGate] = useState(false); const [code, setCode] = useState(''); const [error, setError] = useState(''); function tryEnterAdmin() { if (code === ADMIN_CODE) { setShowAdminGate(false); setCode(''); setError(''); setView('admin'); } else setError('Codice errato'); } return (
📖 Libro per Sorridere
{view === 'admin' && } {view === 'parent' && }
{showAdminGate && (
setShowAdminGate(false)}>
e.stopPropagation()}>
Accesso gestione
{ setCode(e.target.value); setError(''); }} placeholder="Codice" style={styles.gateInput} autoFocus onKeyDown={(e) => e.key === 'Enter' && tryEnterAdmin()} /> {error &&
{error}
}
Demo: 1234
)}
); } // ============================================================ // PARENT FLOW // ============================================================ function ParentFlow({ istituti: tuttiGliIstituti, libri, volantini, ordini, setOrdini, setLibri, paymentLinks }) { const istituti = tuttiGliIstituti.filter((i) => i.attivo !== false); const [step, setStep] = useState(1); const [icId, setIcId] = useState(''); const [tipoScuola, setTipoScuola] = useState(''); const [plesso, setPlesso] = useState(''); const [selezionati, setSelezionati] = useState({}); const [classe, setClasse] = useState(''); const [classeTouched, setClasseTouched] = useState(false); const [nomeBambino, setNomeBambino] = useState(''); const [nomeBambinoTouched, setNomeBambinoTouched] = useState(false); const [telefono, setTelefono] = useState(''); const [metodoPagamento, setMetodoPagamento] = useState('nexi'); const [submitting, setSubmitting] = useState(false); const icSelezionato = istituti.find((i) => i.id === icId); const volantino = icSelezionato ? volantini.find((v) => v.id === icSelezionato.volantinoId) : null; const libriDelVolantino = volantino ? volantino.libriIds .map((id) => libri.find((l) => l.id === id)) .filter(Boolean) .filter((l) => l.attivo !== false) .filter((l) => !l.fasceEta || l.fasceEta.length === 0 || l.fasceEta.includes(tipoScuola)) : []; const libriScelti = Object.keys(selezionati).filter((id) => selezionati[id]).map((id) => libri.find((l) => l.id === id)).filter(Boolean); const totale = libriScelti.reduce((s, l) => s + l.prezzo, 0); const classeValida = classe.trim().length > 0; const nomeBambinoValido = nomeBambino.trim().length > 0; const plessiFiltrati = icSelezionato ? icSelezionato.plessi.filter((p) => p.tipo === tipoScuola) : []; function toggleLibro(libro) { const disponibili = libro.scorte - (libro.venduti || 0); if (disponibili <= 0 && !selezionati[libro.id]) return; setSelezionati((prev) => ({ ...prev, [libro.id]: !prev[libro.id] })); } function reset() { setStep(1); setIcId(''); setTipoScuola(''); setPlesso(''); setSelezionati({}); setClasse(''); setClasseTouched(false); setNomeBambino(''); setNomeBambinoTouched(false); setTelefono(''); setMetodoPagamento('nexi'); } async function confermaOrdine() { if (!classeValida) { setClasseTouched(true); return; } setSubmitting(true); const nuovoOrdine = { id: 'ord-' + Date.now(), data: new Date().toISOString(), istitutoId: icId, istitutoNome: icSelezionato.nome, comune: icSelezionato.comune, tipoScuola: tipoScuola === 'infanzia' ? 'Infanzia/Asilo' : 'Primaria/Media', plesso, classe: classe.trim(), bambino: nomeBambino.trim(), telefono: telefono.trim(), libri: libriScelti.map((l) => ({ id: l.id, titolo: l.titolo, prezzo: l.prezzo })), totale, metodoPagamento, stato: 'da pagare', }; const next = [nuovoOrdine, ...ordini]; await setOrdini(next); const libriAggiornati = libri.map((l) => { const scelto = libriScelti.find((s) => s.id === l.id); return scelto ? { ...l, venduti: (l.venduti || 0) + 1 } : l; }); await setLibri(libriAggiornati); setSubmitting(false); const link = paymentLinks?.[metodoPagamento]; if (link) window.open(link, '_blank', 'noopener,noreferrer'); setStep(5); } if (istituti.length === 0) return
; return (
{step === 1 && (
{istituti.map((ic) => ( ))}
{icSelezionato && (
Il tuo bambino frequenta…
)} {icSelezionato && tipoScuola && (
Che scuola frequenta?
{plessiFiltrati.length === 0 ? (
Nessun plesso di questo tipo registrato per questo istituto.
) : (
{plessiFiltrati.map((p) => ( ))}
)}
)} setStep(2)} nextDisabled={!icId || !tipoScuola || !plesso} nextLabel="Continua" />
)} {step === 2 && ( {libriDelVolantino.length === 0 ? (
Nessun volantino assegnato a questa scuola al momento.
) : (
{libriDelVolantino.map((l) => { const disponibili = l.scorte - (l.venduti || 0); const esaurito = disponibili <= 0; const scelto = !!selezionati[l.id]; return ( ); })}
)} {libriScelti.length > 0 && (
{libriScelti.length} libr{libriScelti.length === 1 ? 'o' : 'i'} selezionat{libriScelti.length === 1 ? 'o' : 'i'} {totale}€
)} setStep(1)} onNext={() => setStep(3)} nextDisabled={libriScelti.length === 0} nextLabel="Continua" />
)} {step === 3 && (
Classe obbligatorio
setClasse(e.target.value)} onBlur={() => setClasseTouched(true)} placeholder="Es. 3A" autoFocus /> {classeTouched && !classeValida &&
Inserisci la classe per continuare.
}
Nome e cognome del bambino obbligatorio
setNomeBambino(e.target.value)} onBlur={() => setNomeBambinoTouched(true)} placeholder="Es. Giulia Bianchi" /> {nomeBambinoTouched && !nomeBambinoValido &&
Inserisci il nome del bambino per continuare.
}
Telefono facoltativo
setTelefono(e.target.value)} type="tel" /> setStep(2)} onNext={() => { setClasseTouched(true); setNomeBambinoTouched(true); if (classeValida && nomeBambinoValido) setStep(4); }} nextDisabled={!classeValida || !nomeBambinoValido} nextLabel="Vai al pagamento" />
)} {step === 4 && (
{libriScelti.map((l) => )}
Come vuoi pagare?
I libri verranno consegnati a scuola, direttamente in classe. Premendo «Conferma e paga» confermi l'ordine e verrai indirizzato alla pagina di pagamento sicura {metodoPagamento === 'paypal' ? 'di PayPal' : 'Nexi'}.
)} {step === 5 && (
Grazie! {libriScelti.length} libr{libriScelti.length === 1 ? 'o' : 'i'} per {nomeBambino} ({classe}) {libriScelti.length === 1 ? 'è stato registrato' : 'sono stati registrati'}. Verranno consegnati a scuola al prossimo passaggio.
)}
); } function Hero() { return (
Scegli i libri,
in meno di un minuto.
Ordina e paga qui, i libri arrivano in classe.
); } function EmptyStateNoSchools() { return (
Nessuna scuola attiva al momento
Il modulo riaprirà non appena verrà attivata la prossima raccolta ordini.
); } function ProgressDots({ step }) { return
{[1, 2, 3, 4].map((n) =>
)}
; } function StepCard({ eyebrow, title, children }) { return (
{eyebrow}
{title}
{children}
); } function NavRow({ onBack, onNext, nextDisabled, nextLabel }) { return (
{onBack ? :
}
); } function SummaryRow({ label, value, big }) { return (
{label} {value}
); } function WhatsAppBand() { return ( 💬 Preferisci scrivere? Ordina su WhatsApp in 30 secondi ); } // ============================================================ // ADMIN PANEL // ============================================================ function AdminPanel({ istituti, setIstituti, libri, setLibri, volantini, setVolantini, ordini, setOrdini, paymentLinks, setPaymentLinks }) { const [tab, setTab] = useState('scuole'); return (
setTab('scuole')} label="Scuole attive" /> setTab('libri')} label="Catalogo libri" /> setTab('volantini')} label="Volantini" /> setTab('pagamenti')} label="Pagamenti" /> setTab('ordini')} label={`Ordini (${ordini.length})`} /> setTab('impostazioni')} label="Impostazioni" />
{tab === 'scuole' && } {tab === 'libri' && } {tab === 'volantini' && } {tab === 'pagamenti' && } {tab === 'ordini' && } {tab === 'impostazioni' && ( )}
); } function AdminTabBtn({ active, onClick, label }) { return ; } function SchoolsAdmin({ istituti, setIstituti, volantini }) { const [nome, setNome] = useState(''); const [comune, setComune] = useState(''); const [via, setVia] = useState(''); const [cap, setCap] = useState(''); const [plessi, setPlessi] = useState([]); const [nuovoPlessoNome, setNuovoPlessoNome] = useState(''); const [nuovoPlessoTipo, setNuovoPlessoTipo] = useState('infanzia'); const [volantinoId, setVolantinoId] = useState(volantini[0]?.id || ''); const [editId, setEditId] = useState(null); function resetForm() { setNome(''); setComune(''); setVia(''); setCap(''); setPlessi([]); setNuovoPlessoNome(''); setNuovoPlessoTipo('infanzia'); setVolantinoId(volantini[0]?.id || ''); setEditId(null); } function startEdit(ic) { setEditId(ic.id); setNome(ic.nome); setComune(ic.comune); setVia(ic.via || ''); setCap(ic.cap || ''); setPlessi(ic.plessi || []); setVolantinoId(ic.volantinoId || volantini[0]?.id || ''); } function aggiungiPlesso() { if (!nuovoPlessoNome.trim()) return; setPlessi((prev) => [...prev, { nome: nuovoPlessoNome.trim(), tipo: nuovoPlessoTipo }]); setNuovoPlessoNome(''); } function rimuoviPlesso(idx) { setPlessi((prev) => prev.filter((_, i) => i !== idx)); } async function salva() { if (!nome.trim() || !comune.trim() || plessi.length === 0) return; if (editId) { const next = istituti.map((i) => i.id === editId ? { ...i, nome: nome.trim(), comune: comune.trim(), via: via.trim(), cap: cap.trim(), plessi, volantinoId } : i); await setIstituti(next); } else { const nuovo = { id: 'ic-' + Date.now(), nome: nome.trim(), comune: comune.trim(), via: via.trim(), cap: cap.trim(), plessi, volantinoId }; await setIstituti([...istituti, nuovo]); } resetForm(); } async function rimuovi(id) { await setIstituti(istituti.filter((i) => i.id !== id)); if (editId === id) resetForm(); } async function toggleAttivo(id) { const next = istituti.map((i) => i.id === id ? { ...i, attivo: i.attivo === false ? true : false } : i); await setIstituti(next); } return (
I tuoi istituti
Disattiva un istituto a fine campagna per non doverlo reinserire l'anno dopo.
{istituti.length === 0 &&
Nessun istituto inserito.
}
{istituti.map((ic) => { const v = volantini.find((vv) => vv.id === ic.volantinoId); const infanzia = (ic.plessi || []).filter((p) => p.tipo === 'infanzia'); const primMedia = (ic.plessi || []).filter((p) => p.tipo === 'primaria_media'); const attivo = ic.attivo !== false; return (
{ic.nome} {!attivo && disattivato}
{ic.comune} {ic.cap ? `· ${ic.cap}` : ''}
{ic.via &&
Segreteria: {ic.via}
} {infanzia.length > 0 &&
🧸 Infanzia: {infanzia.map((p) => p.nome).join(' · ')}
} {primMedia.length > 0 &&
🎒 Primaria/Media: {primMedia.map((p) => p.nome).join(' · ')}
}
📋 {v ? v.nome : 'Nessun volantino assegnato'}
); })}
{editId ? 'Modifica istituto' : 'Aggiungi nuovo istituto'}
Nome istituto comprensivo
setNome(e.target.value)} placeholder="Es. Area Alpi" />
Comune
setComune(e.target.value)} placeholder="Es. Sarzana" />
Via della segreteria
setVia(e.target.value)} placeholder="Es. Via Bertoloni 11" />
CAP
setCap(e.target.value)} placeholder="Es. 19038" />
Plessi
{plessi.length > 0 && (
{plessi.map((p, idx) => (
{p.tipo === 'infanzia' ? '🧸' : '🎒'} {p.nome}
))}
)}
setNuovoPlessoNome(e.target.value)} placeholder="Nome plesso, es. Primaria Falcone" onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), aggiungiPlesso())} />
Volantino da mostrare ai genitori
{volantini.length === 0 ? (
Crea prima un volantino nella tab "Volantini".
) : ( )}
{editId && }
); } function BooksAdmin({ libri, setLibri }) { const [titolo, setTitolo] = useState(''); const [descrizione, setDescrizione] = useState(''); const [pagine, setPagine] = useState(''); const [prezzo, setPrezzo] = useState(''); const [scorte, setScorte] = useState(''); const [copertina, setCopertina] = useState('📘'); const [copertinaImg, setCopertinaImg] = useState(null); const [fasceEta, setFasceEta] = useState(['infanzia', 'primaria_media']); const [editId, setEditId] = useState(null); const [fileError, setFileError] = useState(''); const fileInputRef = useRef(null); function resetForm() { setTitolo(''); setDescrizione(''); setPagine(''); setPrezzo(''); setScorte(''); setCopertina('📘'); setCopertinaImg(null); setFasceEta(['infanzia', 'primaria_media']); setEditId(null); setFileError(''); } function startEdit(l) { setEditId(l.id); setTitolo(l.titolo); setDescrizione(l.descrizione || ''); setPagine(l.pagine || ''); setPrezzo(l.prezzo); setScorte(l.scorte ?? ''); setCopertina(l.copertina || '📘'); setCopertinaImg(l.copertinaImg || null); setFasceEta(l.fasceEta && l.fasceEta.length > 0 ? l.fasceEta : ['infanzia', 'primaria_media']); } function toggleFascia(f) { setFasceEta((prev) => prev.includes(f) ? prev.filter((x) => x !== f) : [...prev, f]); } function handleFile(e) { const file = e.target.files?.[0]; if (!file) return; setFileError(''); const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { const MAX_DIM = 500; let { width, height } = img; if (width > height && width > MAX_DIM) { height = Math.round(height * (MAX_DIM / width)); width = MAX_DIM; } else if (height > MAX_DIM) { width = Math.round(width * (MAX_DIM / height)); height = MAX_DIM; } const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, width, height); ctx.drawImage(img, 0, 0, width, height); let quality = 0.82; let dataUrl = canvas.toDataURL('image/jpeg', quality); while (dataUrl.length > 180000 && quality > 0.3) { quality -= 0.1; dataUrl = canvas.toDataURL('image/jpeg', quality); } if (dataUrl.length > 180000) { setFileError('Immagine troppo pesante anche dopo la compressione: prova con una foto più semplice.'); return; } setCopertinaImg(dataUrl); }; img.onerror = () => setFileError('Non riesco a leggere questa immagine, riprova con un altro file.'); img.src = reader.result; }; reader.onerror = () => setFileError('Errore durante la lettura del file.'); reader.readAsDataURL(file); } async function salva() { if (!titolo.trim() || !prezzo || fasceEta.length === 0) return; if (editId) { const next = libri.map((l) => l.id === editId ? { ...l, titolo: titolo.trim(), descrizione: descrizione.trim(), pagine: pagine ? Number(pagine) : null, prezzo: Number(prezzo), scorte: scorte === '' ? 9999 : Number(scorte), copertina, copertinaImg, fasceEta, } : l); await setLibri(next); } else { const nuovo = { id: 'l-' + Date.now(), titolo: titolo.trim(), descrizione: descrizione.trim(), pagine: pagine ? Number(pagine) : null, prezzo: Number(prezzo), scorte: scorte === '' ? 9999 : Number(scorte), venduti: 0, copertina, copertinaImg, fasceEta, }; await setLibri([...libri, nuovo]); } resetForm(); } async function rimuovi(id) { await setLibri(libri.filter((l) => l.id !== id)); } async function toggleAttivo(id) { const next = libri.map((l) => l.id === id ? { ...l, attivo: l.attivo === false ? true : false } : l); await setLibri(next); } return (
Catalogo attuale
{libri.map((l) => { const disponibili = (l.scorte ?? 9999) - (l.venduti || 0); const fasce = l.fasceEta && l.fasceEta.length > 0 ? l.fasceEta : ['infanzia', 'primaria_media']; const etichettaFasce = fasce.length === 2 ? 'Infanzia + Primaria/Media' : fasce.includes('infanzia') ? 'Solo Infanzia' : 'Solo Primaria/Media'; const attivo = l.attivo !== false; return (
{l.copertinaImg ? : {l.copertina}}
{l.titolo} {!attivo && disattivato}
{l.prezzo}€ · {disponibili <= 0 ? 'Esaurito' : `${disponibili} disponibili`} (venduti {l.venduti || 0}/{l.scorte ?? '∞'})
🎯 {etichettaFasce}
); })}
{editId ? 'Modifica libro' : 'Aggiungi libro'}
Titolo
setTitolo(e.target.value)} placeholder="Titolo del libro" />
Descrizione breve