const { useState, useEffect, useRef, useMemo } = React; // ============ helpers ============ function parsePrice(str) { // Returns { min, max } in EUR, or null for non-numeric (Gratuita, %, etc.) if (!str) return null; const s = str.replace(/\./g, "").replace(/\s/g, ""); // Match all numbers const nums = [...s.matchAll(/(\d+)/g)].map(m => parseInt(m[1], 10)); if (nums.length === 0) return null; // Filter out tiny numbers from things like "10%" — if string contains %, skip if (s.includes("%")) return null; if (/^da/i.test(str.trim())) { return { min: nums[0], max: nums[0] * 1.6, fromOnly: true }; } if (nums.length === 1) return { min: nums[0], max: nums[0] }; return { min: nums[0], max: nums[1] }; } function formatEuro(n) { return "€" + Math.round(n).toLocaleString("it-IT"); } // ============ Cart Context (simple) ============ function useCart() { const [items, setItems] = useState([]); const add = (item) => setItems(prev => prev.find(i => i.id === item.id) ? prev : [...prev, item]); const remove = (id) => setItems(prev => prev.filter(i => i.id !== id)); const has = (id) => items.some(i => i.id === id); return { items, add, remove, has }; } // ============ Sidebar ============ function Sidebar({ active, onNavigate }) { const sections = window.NISBA_DATA.sections; const [menuOpen, setMenuOpen] = useState(false); const handleNav = (slug) => { setMenuOpen(false); onNavigate(slug); }; const allNavItems = [ ...sections.map(s => ({ slug: s.slug, num: s.n, title: s.title })), { slug: "pacchetti", num: "★", title: "Pacchetti consigliati" }, { slug: "tariffario", num: "€/h", title: "Tariffario orario" }, ]; return ( <> ); } // ============ Hero ============ function Hero() { return (
Listino interno · v2026.01
— Chi siamo

Nisba nasce da un'idea semplice: al cliente non serve nient'altro. Niente fronzoli, niente moduli inutili, niente sovrastrutture da agenzia. Solo ciò che serve davvero, costruito su misura — sito, e-commerce, strategia, business plan, finanza agevolata — con la profondità di un partner e la leggerezza di chi sa dove guardare.

nis·ba · contrazione di “non serve nient'altro” — l'essenziale, fatto bene.

Servizi, valore
e misura giusta.

Il presente prezziario rappresenta una base di riferimento interna per la formulazione di offerte, preventivi e proposte commerciali. I corrispettivi potranno essere adattati in base a complessità, urgenza, personalizzazioni e durata del progetto.

Sezioni
10
Voci servizio
62
Pacchetti
5
Valuta
EUR
); } // ============ Section ============ function Section({ section, cart }) { return (
{section.n}
Capitolo {section.n} · {section.items.length} voci

{section.title}

{String(section.items.length).padStart(2, "0")} / VOCI
{section.items.map((item, i) => { const [name, price, note] = item; const id = `${section.slug}-${i}`; const inCart = cart.has(id); const featured = /gratuita|da €/i.test(price); return (
inCart ? cart.remove(id) : cart.add({ id, name, price, category: section.title, parsed: parsePrice(price) })} > {String(i + 1).padStart(2, "0")}
{name}
{price}
{note}
); })}
); } // ============ Packages ============ function PackagesSection({ cart }) { const pkgs = window.NISBA_DATA.packages; return (
Bundle · soluzioni preconfigurate

Pacchetti commerciali consigliati

{String(pkgs.length).padStart(2, "0")} / BUNDLE
{pkgs.map((p, i) => (
{ const id = `pkg-${i}`; if (cart.has(id)) cart.remove(id); else cart.add({ id, name: p.name, price: p.price, category: "Pacchetto", parsed: parsePrice(p.price) }); }} style={{ cursor: "pointer" }} >
PKG · 0{i + 1}

{p.name}

{p.content}

Investimento{p.price}
))}
); } // ============ Hourly ============ function HourlySection() { const items = window.NISBA_DATA.hourly; return (
€/h
Riferimento interno · per attività non a forfait

Tariffario orario

{String(items.length).padStart(2, "0")} / FASCE
{items.map((h, i) => (
{h[0]} {h[1]}
))}
); } // ============ Footer ============ function FooterNote() { return (
— Nota di chiusura

“I prezzi indicati sono da intendersi come riferimento interno e potranno variare in funzione della complessità del progetto, dell'urgenza, delle personalizzazioni richieste e dell'eventuale continuità del rapporto.”

NISBA · Listino interno Edizione 2026 — v.01
); } // ============ Cart Panel ============ function CartPanel({ cart, open, onClose }) { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [message, setMessage] = useState(""); const [sent, setSent] = useState(false); const totals = useMemo(() => { let min = 0, max = 0, hasUnknown = false, hasFrom = false; cart.items.forEach(i => { const p = i.parsed; if (!p) { hasUnknown = true; return; } min += p.min; max += p.max; if (p.fromOnly) hasFrom = true; }); return { min, max, hasUnknown, hasFrom }; }, [cart.items]); const totalText = cart.items.length === 0 ? "—" : (totals.min === totals.max ? formatEuro(totals.min) : `${formatEuro(totals.min)} – ${formatEuro(totals.max)}`); const handleSend = () => { if (cart.items.length === 0 && !message.trim()) return; const lines = []; lines.push("Richiesta di preventivo — Nisba"); lines.push("=".repeat(40)); lines.push(""); if (name) lines.push(`Cliente: ${name}`); if (email) lines.push(`Email: ${email}`); if (name || email) lines.push(""); lines.push("VOCI SELEZIONATE"); lines.push("-".repeat(40)); if (cart.items.length === 0) { lines.push("(nessuna voce selezionata dal listino)"); } else { cart.items.forEach((i, idx) => { lines.push(`${String(idx + 1).padStart(2, "0")}. [${i.category}] ${i.name}`); lines.push(` Prezzo indicativo: ${i.price}`); }); } lines.push(""); lines.push(`Totale stimato: ${totalText}`); if (totals.hasUnknown) lines.push("(+ voci a tariffa variabile)"); lines.push(""); if (message.trim()) { lines.push("ESIGENZE E DETTAGLI DEL CLIENTE"); lines.push("-".repeat(40)); lines.push(message.trim()); lines.push(""); } lines.push("—"); lines.push("Inviato dal Prezziario Nisba · " + new Date().toLocaleDateString("it-IT")); const subject = `Richiesta preventivo Nisba${name ? " — " + name : ""}`; const body = lines.join("\n"); const url = `mailto:amatoremanuele@gmail.com?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; window.location.href = url; setSent(true); setTimeout(() => setSent(false), 4000); }; const canSend = (cart.items.length > 0 || message.trim().length > 0); return ( <>