const { useState, useEffect, useRef } = React;
/* ===========================================================
RESPONSIVE — viewport hook
=========================================================== */
function useMediaQuery(query) {
const [matches, setMatches] = useState(() =>
typeof window !== "undefined" ? window.matchMedia(query).matches : false
);
useEffect(() => {
const mql = window.matchMedia(query);
const handler = (e) => setMatches(e.matches);
setMatches(mql.matches);
mql.addEventListener ? mql.addEventListener("change", handler) : mql.addListener(handler);
return () => {
mql.removeEventListener ? mql.removeEventListener("change", handler) : mql.removeListener(handler);
};
}, [query]);
return matches;
}
const MOBILE = "(max-width: 768px)";
const TABLET = "(max-width: 1080px)";
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"palette": ["#f7f0e2", "#2a1810", "#c1813c"],
"showTraditional": true
}/*EDITMODE-END*/;
/* ===========================================================
DATA
=========================================================== */
const PRODUCTS = [
{ id: 1, cat: "viennoiserie", name: "Croissant au beurre", subtitle: "feuilleté 72h", desc: "Pâte feuilletée pliée à la main, beurre fermier, sortie à 6h du matin.", price: 1.8, weight: "85g", tag: "Signature", img: "assets/croissants.jpg", color: "#caa86a" },
{ id: 2, cat: "viennoiserie", name: "Pain au chocolat", subtitle: "double barre noire", desc: "Deux barres de chocolat noir 70%, feuilletage doré à cœur.", price: 2.2, weight: "95g", img: null, color: "#7a4a2a" },
{ id: 3, cat: "viennoiserie", name: "Brioche pistache", subtitle: "filée à la main", desc: "Brioche tressée, crème de pistache, sucre perlé.", price: 3.5, weight: "180g", tag: "Nouveau", img: null, color: "#a8c478" },
{ id: 4, cat: "sale", name: "Pâté thon", subtitle: "harissa douce, olives", desc: "Thon Méditerranée, œuf, harissa et olives — tout maison.", price: 3.5, weight: "200g", img: null, color: "#c4a978" },
{ id: 5, cat: "sale", name: "Pâté fromage", subtitle: "triple fromage", desc: "Emmental, mozzarella, feta. Herbes du jardin, pâte brisée maison.", price: 3.2, weight: "200g", img: null, color: "#e8d49a" },
{ id: 6, cat: "sale", name: "Pâté poulet", subtitle: "champignons & béchamel", desc: "Poulet rôti effiloché, champignons de Paris, béchamel.", price: 4.0, weight: "220g", img: null, color: "#b88a5a" },
{ id: 7, cat: "oriental", name: "Mhalbiya", subtitle: "amandes & miel", desc: "Rouleaux croustillants garnis d'amandes pilées, parfumés au miel d'oranger.", price: 5.5, weight: "150g", tag: "Tradition", img: "assets/mhalbiya.jpg", color: "#b89858" },
{ id: 8, cat: "oriental", name: "Deblas", subtitle: "feuilles d'or au miel", desc: "Pâte fine roulée en rose, frite, trempée dans le miel, parsemée de sésame.", price: 4.5, weight: "120g", tag: "Maison", img: "assets/deblas.jpg", color: "#d9a958" },
{ id: 9, cat: "oriental", name: "Zlabia", subtitle: "miel & sésame doré", desc: "Spirales croustillantes au miel d'oranger, sésame doré.", price: 4.0, weight: "130g", img: "assets/zlabia.jpg", color: "#c98b3a" },
];
const CATS = [
{ id: "all", label: "Toute la carte" },
{ id: "viennoiserie", label: "Viennoiseries" },
{ id: "sale", label: "Salés" },
{ id: "oriental", label: "Pâtisseries orientales" },
];
/* ===========================================================
ICONS — minimal hairline
=========================================================== */
const Icon = {
arrow: (p) => ,
arrowUR: (p) => ,
plus: (p) => ,
bag: (p) => ,
close: (p) => ,
menu: (p) => ,
pin: (p) => ,
clock: (p) => ,
phone: (p) => ,
insta: (p) => ,
fb: (p) => ,
wa: (p) => ,
};
/* ===========================================================
ORNAMENT — small decorative SVG flourish
=========================================================== */
function Flourish({ color = "currentColor", width = 96 }) {
return (
);
}
/* ===========================================================
BRAND
=========================================================== */
function CroissantMark({ size = 26, color = "currentColor" }) {
return (
);
}
function Brandmark({ size = "md", color }) {
const c = color || "var(--ink)";
const fs = size === "lg" ? 32 : size === "sm" ? 16 : 22;
return (
);
}
/* ===========================================================
TOP BAR
=========================================================== */
function TopBar() {
const mob = useMediaQuery(MOBILE);
return (
Ouvert · 06h – 20h
{!mob && · }
{!mob && Livraison à Bizerte centre }
{!mob && (
« Pétri à la main, cuit chaque matin »
)}
);
}
/* ===========================================================
NAV — centered, elegant
=========================================================== */
function Nav({ cartCount, onCart, onNav }) {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const mob = useMediaQuery(MOBILE);
useEffect(() => {
const f = () => setScrolled(window.scrollY > 60);
window.addEventListener("scroll", f);
return () => window.removeEventListener("scroll", f);
}, []);
const left = [["La boutique", "shop"], ["Spécialités", "specialty"]];
const right = [["La maison", "about"], ["Nous trouver", "contact"]];
const all = [...left, ...right];
const go = (id) => { setMenuOpen(false); onNav(id); };
const linkStyle = { fontSize: 13, letterSpacing: "0.06em", textTransform: "uppercase", color: "var(--ink-2)" };
const iconBtn = {
background: "transparent", border: "1px solid var(--line)", borderRadius: 999,
width: 42, height: 42, display: "flex", alignItems: "center", justifyContent: "center",
};
return (
);
}
/* ===========================================================
HERO
=========================================================== */
function Hero({ onNav }) {
const mob = useMediaQuery(MOBILE);
return (
{/* LEFT — TEXT */}
Pâtisserie · Bizerte · depuis 2018
L'art doux
du croissant ,
du miel
et de la patience.
Une maison de pâtisserie au cœur de Bizerte, où le feuilletage français
rencontre la générosité tunisienne. Tout est fait à la main, cuit du jour, jamais conservé.
onNav("shop")} className="btn-primary">
Découvrir la carte
onNav("contact")} className="btn-ghost">
Nous trouver
{/* TRUST */}
{[
["72h", "de feuilletage"],
["100%", "fait maison"],
["4.9★", "312 avis Google"],
].map(([n, l], i) => (
))}
{/* RIGHT — PHOTO COMPOSITION */}
{/* Background frame */}
{/* Main photo */}
{/* small photo overlay */}
{/* circular badge */}
fournée du
matin
à 6 heures
);
}
/* ===========================================================
MARQUEE — italic caramel band
=========================================================== */
function Marquee() {
const mob = useMediaQuery(MOBILE);
const items = ["Croissant au beurre", "Pâté thon harissa", "Mhalbiya aux amandes", "Brioche pistache", "Deblas au miel", "Pain au chocolat", "Zlabia sésame", "Pâté poulet"];
return (
{Array(3).fill(0).map((_, i) => (
{items.map((x, j) => (
{x}
✦
))}
))}
);
}
/* ===========================================================
PRODUCT CARD
=========================================================== */
function ProductCard({ p, onAdd, size = "md" }) {
const [hover, setHover] = useState(false);
const aspect = size === "tall" ? "3/4" : size === "wide" ? "5/4" : "4/5";
return (
setHover(true)}
onMouseLeave={() => setHover(false)}
style={{ position: "relative" }}
>
{p.img ? (
) : (
<>
{p.name}
douceur maison · photo à venir
>
)}
{p.tag && (
{p.tag}
)}
{/* hover add button */}
onAdd(p)} style={{
position: "absolute", right: 14, bottom: 14,
width: 44, height: 44, borderRadius: "50%",
background: "var(--ink)", color: "var(--paper)",
border: "none",
display: "flex", alignItems: "center", justifyContent: "center",
opacity: hover ? 1 : 0,
transform: hover ? "translateY(0)" : "translateY(8px)",
transition: "all 0.3s ease",
}}>
);
}
/* ===========================================================
CATALOGUE
=========================================================== */
function Catalogue({ onAdd }) {
const [filter, setFilter] = useState("all");
const mob = useMediaQuery(MOBILE);
const tab = useMediaQuery(TABLET);
const filtered = filter === "all" ? PRODUCTS : PRODUCTS.filter(p => p.cat === filter);
return (
La carte
Notre vitrine du jour.
Neuf gourmandises, préparées chaque matin avant l'ouverture.
{/* FILTER */}
{CATS.map(c => (
setFilter(c.id)} style={{
background: filter === c.id ? "var(--ink)" : "transparent",
color: filter === c.id ? "var(--paper)" : "var(--ink-2)",
border: "1px solid " + (filter === c.id ? "var(--ink)" : "var(--line)"),
padding: "12px 22px", borderRadius: 999,
fontSize: 13, letterSpacing: "0.04em",
transition: "all 0.2s ease",
}}>{c.label}
))}
);
}
/* ===========================================================
SPECIALTIES — Tunisian sweets feature
=========================================================== */
function Specialty({ onAdd }) {
const mob = useMediaQuery(MOBILE);
const tab = useMediaQuery(TABLET);
const specs = PRODUCTS.filter(p => p.cat === "oriental");
return (
Spécialités tunisiennes
Les douceurs de la maison.
Recettes héritées des grands-mères de Bizerte. Le miel d'oranger, le sésame doré et les amandes pilées, dans un feuilleté patiemment roulé.
{specs.map((p, i) => (
))}
);
}
function SpecCard({ p, idx, onAdd }) {
const [hover, setHover] = useState(false);
return (
setHover(true)}
onMouseLeave={() => setHover(false)}
style={{ position: "relative" }}
>
{p.img && (
)}
n° {String(idx + 1).padStart(2, "0")}
{p.name}
{p.subtitle}
{p.desc}
{p.price.toFixed(1)} DT / {p.weight}
onAdd(p)} style={{
background: "transparent", border: "1px solid rgba(252,247,235,0.5)",
color: "var(--paper)", borderRadius: 999,
padding: "8px 16px", fontSize: 12, letterSpacing: "0.04em",
display: "flex", alignItems: "center", gap: 8,
}}>
Ajouter
);
}
/* ===========================================================
ABOUT
=========================================================== */
function About() {
const mob = useMediaQuery(MOBILE);
return (
{/* PHOTO COMPOSITION */}
{/* italic caption */}
Dans notre atelier de Bizerte, juste avant l'aube.
{/* TEXT */}
La maison
Une famille,
un atelier,
une seule façon de faire.
Pasticcini est né en 2018 dans une cuisine modeste, à deux pas du Vieux Port. La maison a grandi sans jamais changer de méthode : tout est encore pétri à la main, le feuilletage repose toute la nuit, et chaque pièce est cuite le matin même.
Les viennoiseries empruntent à la rigueur française. Les pâtés rendent hommage à la générosité bizertine. Et nos pâtisseries orientales — mhalbiya, deblas, zlabia — viennent directement des cahiers de recettes de nos grands-mères.
« Le bon goût ne se presse pas. Il se mérite — au four, et au cœur. »
— Aïcha, chef pâtissière
);
}
/* ===========================================================
TESTIMONIALS
=========================================================== */
function Testimonials() {
const reviews = [
{ name: "Yasmine B.", from: "Bizerte", stars: 5, text: "Le seul endroit à Bizerte où je trouve un vrai croissant feuilleté. La pâte est aérienne, le beurre se sent à chaque bouchée." },
{ name: "Mohamed K.", from: "Tunis", stars: 5, text: "Je fais une heure de route chaque samedi pour leurs pâtés thon et leurs mhalbiya. Indétrônables." },
{ name: "Sophie L.", from: "Marseille", stars: 5, text: "On dirait un atelier de pâtisserie parisien posé en bord de Méditerranée. Un service adorable, une vitrine envoûtante." },
];
const mob = useMediaQuery(MOBILE);
const tab = useMediaQuery(TABLET);
return (
Témoignages
On en parle autour du four.
{[1,2,3,4,5].map(i => ★ )}
4.9/5 · 312 avis Google
{reviews.map((r, i) => (
"
{Array(r.stars).fill(0).map((_, j) => ★ )}
{r.text}
))}
);
}
/* ===========================================================
CONTACT
=========================================================== */
function Contact() {
const mob = useMediaQuery(MOBILE);
const [form, setForm] = useState({ name: "", contact: "", topic: "Commande sur mesure", message: "" });
const [sent, setSent] = useState(false);
const submit = (e) => {
e.preventDefault();
setSent(true);
setTimeout(() => { setSent(false); setForm({ name: "", contact: "", topic: "Commande sur mesure", message: "" }); }, 3500);
};
return (
);
}
function Field({ label, value, onChange, placeholder, textarea }) {
const In = textarea ? "textarea" : "input";
return (
{label}
onChange(e.target.value)}
placeholder={placeholder}
rows={textarea ? 4 : undefined}
style={{
width: "100%", background: "transparent",
border: "none", borderBottom: "1px solid var(--line)",
padding: "10px 0", fontSize: 17, fontFamily: "inherit",
color: "var(--ink)", outline: "none",
resize: textarea ? "vertical" : "none",
}}
/>
);
}
/* ===========================================================
FOOTER
=========================================================== */
function Footer() {
const mob = useMediaQuery(MOBILE);
return (
« Pétri à la main, cuit chaque matin. »
{[
{ h: "Adresse", l: ["12 Av. Habib Bourguiba", "Bizerte 7000", "Tunisie"] },
{ h: "Horaires", l: ["Lun–Sam · 06h–20h", "Dimanche · 07h–13h"] },
{ h: "Contact", l: ["+216 72 432 100", "bonjour@pasticcini.tn", "WhatsApp dispo"] },
{ h: "Suivez-nous", l: ["Instagram @pasticcini", "Facebook Pasticcini Bizerte"] },
].map((c, i) => (
))}
© 2026 Pasticcini — Pâtisserie artisanale, Bizerte
Conçu avec soin à Bizerte
);
}
/* ===========================================================
CART DRAWER
=========================================================== */
function CartDrawer({ open, items, onClose, onAdjust }) {
const total = items.reduce((s, x) => s + x.p.price * x.q, 0);
const count = items.reduce((s, x) => s + x.q, 0);
if (!open) return null;
return (
Panier
{count} {count <= 1 ? "article" : "articles"}
{items.length === 0 ? (
Votre panier est vide.
Ajoutez un croissant pour commencer la journée.
) : items.map(({ p, q }) => (
{p.img &&
}
{p.name}
{p.subtitle}
onAdjust(p.id, q - 1)} style={qBtn}>−
{q}
onAdjust(p.id, q + 1)} style={qBtn}>+
{(p.price * q).toFixed(1)} DT
))}
{items.length > 0 && (
)}
);
}
const qBtn = {
background: "var(--paper)", border: "1px solid var(--line)",
borderRadius: "50%", width: 22, height: 22,
display: "flex", alignItems: "center", justifyContent: "center",
fontSize: 13, lineHeight: 1, cursor: "pointer", padding: 0, color: "var(--ink)",
};
/* ===========================================================
APP
=========================================================== */
function App() {
const [cart, setCart] = useState([]);
const [cartOpen, setCartOpen] = useState(false);
const [toast, setToast] = useState(null);
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
useEffect(() => {
const p = t.palette || TWEAK_DEFAULTS.palette;
document.documentElement.style.setProperty("--bg", p[0]);
document.documentElement.style.setProperty("--ink", p[1]);
document.documentElement.style.setProperty("--caramel", p[2]);
}, [t.palette]);
const onAdd = (p) => {
setCart(c => {
const ex = c.find(x => x.p.id === p.id);
if (ex) return c.map(x => x.p.id === p.id ? { ...x, q: x.q + 1 } : x);
return [...c, { p, q: 1 }];
});
setToast(`✓ ${p.name} ajouté`);
setTimeout(() => setToast(null), 1800);
};
const onAdjust = (id, q) => {
if (q <= 0) setCart(c => c.filter(x => x.p.id !== id));
else setCart(c => c.map(x => x.p.id === id ? { ...x, q } : x));
};
const onNav = (id) => {
const el = document.getElementById(id);
if (el) window.scrollTo({ top: id === "top" ? 0 : el.offsetTop - 80, behavior: "smooth" });
};
const count = cart.reduce((s, x) => s + x.q, 0);
return (
<>
setCartOpen(true)} onNav={onNav} />
setCartOpen(false)} onAdjust={onAdjust} />
{toast && (
{toast}
)}
setTweak("palette", v)}
options={[
["#f7f0e2", "#2a1810", "#c1813c"],
["#fcf7eb", "#1a0f08", "#a86530"],
["#f0e8d4", "#3a2818", "#d4a169"],
["#fff8ec", "#2a1810", "#8e5a22"],
["#ebe4d2", "#2e1d10", "#c97548"],
]}
/>
>
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );