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 (
<>
Indice
setMenuOpen(false)} aria-label="Chiudi">×
>
);
}
// ============ 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.
);
}
// ============ 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}
{inCart ? "✓" : "+"}
);
})}
);
}
// ============ 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 (
<>
>
);
}
// ============ App ============
function App() {
const cart = useCart();
const [active, setActive] = useState("consulenza");
const [cartOpen, setCartOpen] = useState(false);
const mainRef = useRef(null);
const onNavigate = (slug) => {
const el = document.getElementById(slug);
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 24;
window.scrollTo({ top, behavior: "smooth" });
}
};
useEffect(() => {
const handler = () => {
const sections = document.querySelectorAll("[data-section]");
let current = "consulenza";
sections.forEach(s => {
const r = s.getBoundingClientRect();
if (r.top < window.innerHeight * 0.35) current = s.getAttribute("data-section");
});
setActive(current);
};
window.addEventListener("scroll", handler, { passive: true });
handler();
return () => window.removeEventListener("scroll", handler);
}, []);
return (
{window.NISBA_DATA.sections.map(s => (
))}
setCartOpen(true)}>
{cart.items.length === 0 ? (
<>
Componi un preventivo
+
>
) : (
<>
Bozza preventivo
{cart.items.length}
>
)}
setCartOpen(false)} />
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );