/* sections.jsx — page sections for La Signora */ const { useState, useEffect, useLayoutEffect, useRef } = React; // ── Reveal-on-scroll wrapper ── function Reveal({ children, as: As = 'div', className = '', ...rest }) { const ref = useRef(null); const [shown, setShown] = useState(false); useLayoutEffect(() => { const el = ref.current; if (!el) return; // Already above the fold — show before first paint, no flash if (el.getBoundingClientRect().top < window.innerHeight) { setShown(true); return; } const io = new IntersectionObserver( ([e]) => { if (e.isIntersecting) { setShown(true); io.disconnect(); } }, { threshold: 0.12, rootMargin: '0px 0px -80px 0px' } ); io.observe(el); return () => io.disconnect(); }, []); return ( {children} ); } // ── Page detection (used by Nav and App) ── function getPage() { const p = window.location.pathname; if (/menu\.html/.test(p)) return 'menu'; if (/find\.html/.test(p)) return 'find'; if (/catering\.html/.test(p)) return 'catering'; if (/contact\.html/.test(p)) return 'contact'; if (/privacy\.html/.test(p)) return 'privacy'; if (/accessibility\.html/.test(p)) return 'accessibility'; return 'home'; } // ── Nav ── const NAV_ITEMS = [ { href: 'index.html', page: 'home', label: 'Home', hint: 'La Signora' }, { href: 'menu.html', page: 'menu', label: 'Menu', hint: 'Arancini & specials' }, { href: 'catering.html', page: 'catering', label: 'Services', hint: 'Private + corporate' }, { href: 'contact.html', page: 'contact', label: 'Contact', hint: 'Get in touch' }, ]; function Nav() { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); const currentPage = getPage(); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 24); onScroll(); window.addEventListener('scroll', onScroll, { passive: true }); return () => window.removeEventListener('scroll', onScroll); }, []); // Close drawer when resizing back to desktop useEffect(() => { const onResize = () => { if (window.innerWidth > 820) setMenuOpen(false); }; window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); // Lock body scroll while drawer is open useEffect(() => { document.body.style.overflow = menuOpen ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [menuOpen]); // Close drawer on Escape useEffect(() => { if (!menuOpen) return; const onKey = (e) => { if (e.key === 'Escape') setMenuOpen(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [menuOpen]); return ( <>
La Signora
{/* Mobile drawer — slides down from top, sits below the sticky nav */}
{NAV_ITEMS.map((item) => ( {item.label} ))} Find Us
); } // ── Hero ── function Hero() { return (
Sicilian street food · Portland, OR

made with love,
From Sicily,
with my mother's
recipes.

Antonietta cooks the food she grew up on in Sicily — slow-braised ragù, hand-shaped arancini, the rotating specials her regulars come back for. No shortcuts, no compromises, and certainly no red-and-white checkered tablecloths.

Find Us See the Menu Order on DoorDash
Cathedral Park Food Carts · St. Johns
La Signora — the arancino mascot in a sun hat and red glasses
Ciao! EST. 2025
); } // ── Marquee strip ── function Strip() { const phrases = [ 'Ragù di Mamma', 'Prosciutto · Pistacchio', 'Mozzarella · Funghi', 'Pasta al Forno', 'Cannoli', 'Tiramisù', 'Lasagna della Domenica' ]; const items = ( {phrases.map((p, i) => ( {p} ))} ); return ( ); } // ── Story ── function Story() { return (
The story

Meet
la Signora.

The food has a name. So does the woman who makes it.

— Antonietta Benigno

Antonietta Benigno grew up in southern Sicily, where Sunday meant her mother at the stove and a kitchen full of people. She built a career on two continents — managing a restaurant in New Jersey, then teaching back in Italy — before crossing the ocean again in 2021 to be near her daughter in Portland.

She wasn't planning on cooking for a living. She was working as a manager here when an accident forced her to leave the job — so she started cooking the way her mother taught her instead, and people noticed. Her Spanish-speaking co-workers used to call her warmly{' '} la señora — she translated it into Italian, and made it the name above her cart.

I'm thinking about the people that will eat my food, and I try to put my heart into it — and they can feel it.

Antonietta · La Signora

Every dish is something her mother taught her: a four-hour ragù that can't be hurried, arancini shaped one by one, sweets she learned standing on a chair in a Sicilian kitchen. If she can't get the right ingredients — some still come from Italy — she won't put the dish on the board.

Every time I make her recipes, it's like she's right there in the kitchen with me.

Sicily New Jersey Milan Portland

As featured in the{' '} Portland Tribune

); } // ── Menu ── const FLAVORS = [ { name: 'Carlentini', shape: 'Classico', city: 'Ragù bolognese', blurb: 'Slow-braised meat sauce from a four-hour recipe her mother taught her. The one she will not rush.', ings: ['Risotto', 'Angus beef', 'Tomato', 'Carrot', 'Onion', 'Celery', 'Peas', 'Mozzarella'], extras: [ ['Vegetarian', 'No'], ['Allergens', 'Gluten, Dairy'], ], palette: ['#B5482B', '#8C341E', '#E0A638'] }, { name: 'Bronte', city: 'Prosciutto & Pistacchio', blurb: 'Salty cured pork, pistachios, mozzarella — the Sicilian flavor pairing that converts every skeptic.', ings: ['Risotto', 'Prosciutto', 'Pistachio cream', 'Mozzarella'], extras: [ ['Vegetarian', 'No'], ['Allergens', 'Gluten, Dairy, Tree nuts'], ], palette: ['#5C6B33', '#3F4A21', '#E6D2A9'], hidden: true, // replaced by Mafalda — keeping the data around in case it comes back }, { name: 'Mafalda', shape: 'Sferico', city: 'Pistacchio, Mortadella & Mozzarella', blurb: 'Mortadella and pistachio, mozzarella folded in — the classic Italian sandwich pairing, reshaped into a cone.', ings: ['Risotto', 'Mortadella', 'Pistachio cream', 'Mozzarella'], extras: [ ['Vegetarian', 'No'], ['Allergens', 'Gluten, Dairy, Tree nuts'], ], palette: ['#5C6B33', '#3F4A21', '#E6D2A9'] }, { name: 'Madonie', shape: 'Allungato', city: 'Mozzarella & Funghi', blurb: 'Mushrooms slow-cooked in extra virgin olive oil, chopped and folded with low-moisture mozzarella. Vegetarian. Quietly the favorite.', ings: ['Risotto', 'Mixed mushrooms', 'Extra virgin olive oil', 'Mozzarella'], extras: [ ['Vegetarian', 'Yes'], ['Allergens', 'Gluten, Dairy'], ], palette: ['#8C341E', '#5C6B33', '#E0A638'] }, { name: 'Siracusa', shape: 'Cilindrico', city: 'Spinaci & Mozzarella', blurb: 'Spinach sautéed in butter, folded with mozzarella. Simple, honest, and somehow the one people keep coming back to.', ings: ['Risotto', 'Spinach', 'Butter', 'Mozzarella'], extras: [ ['Vegetarian', 'Yes'], ['Allergens', 'Gluten, Dairy'], ], palette: ['#5C6B33', '#3F4A21', '#E6D2A9'] }, { name: 'Norma', shape: 'Diamante', city: 'alla Catanese', blurb: 'Fried eggplant, tomato and salted ricotta — the Catania classic, reshaped into a cone.', ings: ['Risotto', 'Fried eggplant', 'Tomato', 'Ricotta salata'], extras: [ ['Vegetarian', 'Yes'], ['Allergens', 'Gluten, Dairy'], ], palette: ['#8C341E', '#5C6B33', '#E0A638'] }, ]; const VISIBLE_FLAVORS = FLAVORS.filter((f) => !f.hidden); function ArancinoCard({ f, idx, open, onToggle }) { return (
N° {String(idx + 1).padStart(2, '0')}

{f.name}

{f.city}

{f.blurb}

{f.ings.map((i, k) => {i})}
{f.extras.map(([k, v], i) => (
{k}{v}
))}
{open ? 'Less' : 'More'}
); } function ArancinoSlider({ flavors }) { const trackRef = useRef(null); const [open, setOpen] = useState(0); const [active, setActive] = useState(0); const [scrollLeft, setScrollLeft] = useState(0); const [maxScroll, setMaxScroll] = useState(0); // measure on mount + resize useEffect(() => { const el = trackRef.current; if (!el) return; const measure = () => setMaxScroll(el.scrollWidth - el.clientWidth); measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, [flavors.length]); // active page = leftmost visible card index const onScroll = () => { const el = trackRef.current; if (!el) return; setScrollLeft(el.scrollLeft); const cards = el.querySelectorAll('.card'); if (!cards.length) return; const left = el.scrollLeft; let bestI = 0, bestD = Infinity; cards.forEach((c, i) => { const d = Math.abs(c.offsetLeft - left); if (d < bestD) { bestD = d; bestI = i; } }); setActive(bestI); }; const scrollToCard = (i) => { const el = trackRef.current; if (!el) return; const card = el.querySelectorAll('.card')[i]; if (!card) return; el.scrollTo({ left: card.offsetLeft, behavior: 'smooth' }); }; const step = (dir) => { const el = trackRef.current; if (!el) return; const cards = el.querySelectorAll('.card'); const next = Math.max(0, Math.min(cards.length - 1, active + dir)); scrollToCard(next); }; const atStart = active <= 0; const atEnd = maxScroll > 0 && scrollLeft >= maxScroll - 4; return (
{flavors.map((f, i) => ( setOpen(open === i ? -1 : i)} /> ))}
{flavors.map((f, i) => (
{String(active + 1).padStart(2, '0')} / {String(flavors.length).padStart(2, '0')}
); } function Menu() { const specials = [ { it: 'Lasagna della Domenica', en: 'Sunday lasagna', tag: 'Heritage' }, { it: 'Pasta al Forno', en: 'Baked pasta', tag: 'Comfort' }, { it: 'Cannoli', en: 'filled fresh when you order', tag: 'Dolce' }, { it: 'Tiramisù', en: 'made the long way', tag: 'Dolce' }, { it: 'Parmigiana di Melanzane', en: 'eggplant parmigiana', tag: 'When she finds them' }, ]; const [lightbox, setLightbox] = useState(null); const lbOuterRef = useRef(null); const lastFocusedRef = useRef(null); useEffect(() => { if (!lightbox) return; // Remember what had focus so it can be restored on close, then move // focus into the dialog (close button) for keyboard/screen-reader users. lastFocusedRef.current = document.activeElement; const closeBtn = lbOuterRef.current && lbOuterRef.current.querySelector('.lb-close'); if (closeBtn) closeBtn.focus(); const onKey = (e) => { if (e.key === 'Escape') { setLightbox(null); return; } if (e.key !== 'Tab' || !lbOuterRef.current) return; // Trap Tab focus inside the dialog while it's open. const focusable = lbOuterRef.current.querySelectorAll( 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])' ); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } }; document.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; if (lastFocusedRef.current) lastFocusedRef.current.focus(); }; }, [lightbox]); const navigate = (dir) => { const idx = VISIBLE_FLAVORS.indexOf(lightbox); setLightbox(VISIBLE_FLAVORS[(idx + dir + VISIBLE_FLAVORS.length) % VISIBLE_FLAVORS.length]); }; const touchStartRef = useRef(null); const onLbTouchStart = (e) => { const t = e.touches[0]; touchStartRef.current = { x: t.clientX, y: t.clientY }; }; const onLbTouchEnd = (e) => { const start = touchStartRef.current; touchStartRef.current = null; if (!start) return; const t = e.changedTouches[0]; const dx = t.clientX - start.x; const dy = t.clientY - start.y; if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy) * 1.5) { navigate(dx < 0 ? 1 : -1); } }; return ( {lightbox && (
setLightbox(null)}>
e.stopPropagation()} onTouchStart={onLbTouchStart} onTouchEnd={onLbTouchEnd} >
{`${lightbox.name}
N° {String(VISIBLE_FLAVORS.indexOf(lightbox) + 1).padStart(2, '0')}

{lightbox.name}

{lightbox.city}

{lightbox.blurb}

{lightbox.ings.map((ing, k) => {ing})}
{lightbox.extras.map(([k, v], i) => (
{k}{v}
))}
)}
); } // ── Anatomy / What are arancini ── function Anatomy() { return (
A short lesson
/a·ran·CHEE·no/

So, what is
an arancino?

A cone of slow-cooked, saffron-touched risotto stuffed with a savory filling, breaded by hand, and fried to the color of an Italian sunset. Sicilian street food at its purest — handheld, warm, and meant to be eaten standing up.

  • 01The shell. A thin, golden crust of toasted breadcrumbs. Audible.
  • 02The rice. Risotto, cooked slowly, kissed with saffron, cooled, shaped.
  • 03The heart. Ragù. Pistachio. Mushroom. The reason you came.
  • 04The name. Arancino or arancina? Sicilians have been arguing about this for centuries. Antonietta makes them in every shape — and calls them whatever she likes.
); } // ── Find Us ── const LOCATIONS = [ { key: 'cathedral', badge: 'The Cart', title: 'Cathedral Park\nFood Carts', type: 'The original food cart', address: ['7431 N. Saint Louise Ave.', 'Portland, OR 97203'], neighborhood: 'St. Johns · six blocks from Cathedral Park', hours: [ { day: 'Tue', time: '3 p.m. — 8 p.m.' }, { day: 'Wed — Fri', time: '12 p.m. — 8 p.m.' }, { day: 'Sat — Sun', time: 'Closed' }, ], accent: 'var(--terracotta)', map: { scene: 'cart' }, mapsUrl: 'https://share.google/LOsRK1SgexllICsvq' }, { key: 'vancouver', badge: 'The Market', title: 'Vancouver\nFarmers Market', type: 'Weekend market booth', address: ['605 Esther St', 'Vancouver, WA 98660'], neighborhood: 'Downtown Vancouver · just over the Columbia', hours: [ { day: 'Sat', time: '9 a.m. — 3 p.m.' }, { day: 'Sun', time: '10 a.m. — 3 p.m.' }, ], accent: 'var(--saffron)', map: { scene: 'market' }, mapsUrl: 'https://share.google/9Iq3qnLnUDuk1FZBa' }, { key: 'gresham', badge: 'The Café', title: 'Simply Coffee', type: 'Café · frozen take-home trays', address: ['455 NE Burnside Rd', 'Gresham, OR 97030'], neighborhood: 'Downtown Gresham', hours: [ { day: 'Daily', time: '7 a.m. — 4 p.m.' }, ], accent: 'var(--olive)', map: { scene: 'cafe' }, mapsUrl: 'https://share.google/w04rvnTsM6kjX0vHr' }, ]; function LocInfoCard({ loc, className = '' }) { return (
{loc.type}

{loc.title.split('\n').map((line, k, arr) => ( {k === arr.length - 1 ? {line} : <>{line}
}
))}

Address

{loc.address.map((l, k) => {l}
)} {loc.neighborhood}

{loc.mapsUrl && ( View on Google Maps )}
Hours

{loc.hours.map((h, k) => ( {h.day} {' · '}{h.time} {k < loc.hours.length - 1 &&
}
))}

); } function Find() { const [i, setI] = useState(0); const [cardMinH, setCardMinH] = useState(0); const measureRef = useRef(null); // Measure the tallest info-card across all locations so the grid height // is locked and doesn't jump between locations. React.useLayoutEffect(() => { const el = measureRef.current; if (!el) return; const measure = () => { const cards = el.querySelectorAll('.info-card'); let max = 0; cards.forEach((c) => { max = Math.max(max, c.offsetHeight); }); if (max > 0) setCardMinH(max); }; measure(); const ro = new ResizeObserver(measure); ro.observe(el); if (document.fonts && document.fonts.ready) document.fonts.ready.then(measure); return () => ro.disconnect(); }, []); const loc = LOCATIONS[i]; const next = () => setI((x) => (x + 1) % LOCATIONS.length); const prev = () => setI((x) => (x - 1 + LOCATIONS.length) % LOCATIONS.length); const minH = cardMinH || undefined; return (
Come hungry

Find la Signora.

Three places, one Sicilian mother. The cart, the market, and a café shelf full of frozen Sunday lunch.

{LOCATIONS.map((l, k) => ( ))}
{/* hidden measurement layer — renders every location's info-card inside the same grid template so each gets the correct column width and we can pick the tallest one */}
); } // ── Reviews ── const REVIEWS = [ { quote: 'My Sicilian family and I grew up with the best Italian food in CT, and it\'s been so hard to find anything close out here. Signora Antonietta knows how to make arancini right — the ragù was exactly the real deal. 10/10, would recommend.', name: 'Vanessa S.', where: 'Local Guide · July 2026', via: 'Google', stars: 5, }, { quote: 'La Signora feels like a little piece of home in Portland. Whenever I miss the flavors of Sicily, this is my go-to. Amazing arancini, lasagna, and cannoli — authentic, comforting, and always worth it.', name: 'Samuele B.', where: 'June 2026', via: 'Google', stars: 5, }, { quote: 'When I was pregnant I was always craving arancini — if I\'d known there was such an authentic place making them, I\'d have gone every day! Absolutely delicious and delicate, and finally somewhere that doesn\'t overwhelm the food with spices or garlic. The cannoli are very good too: the shell is crispy, which means they\'re fresh.', name: 'Raffaella G.', where: 'Local Guide · June 2026', via: 'Google', stars: 5, }, { quote: 'Came here on a whim and the cannoli are unreal. Perfect crunch, creamy filling, just the right sweetness. Easily some of the best I\'ve had.', name: 'Michael D.', where: 'May 2026', via: 'Google', stars: 5, }, { quote: 'Delizioso! Un\'oasi della cucina Siciliana a Portland. We shared three types of arancini and almond granita — all delicious and well made. She even helped us practice our Italian. Highly recommend!', name: 'Katherine S.', where: 'June 2026', via: 'Google', stars: 5, }, { quote: 'As someone who\'s half Sicilian, it\'s amazing to see arancino on the menu! So many flavors to choose from — go support them. I\'m saving the cannolo for later.', name: 'Jeremy F.', where: 'June 2026', via: 'Google', stars: 5, }, ]; function Stars({ n = 5, color = 'var(--saffron)' }) { return ( {Array.from({ length: 5 }).map((_, i) => ( ))} ); } function getPerPage() { if (window.innerWidth <= 680) return 1; if (window.innerWidth <= 980) return 2; return 3; } function ReviewsSlider() { const [perPage, setPerPage] = useState(() => getPerPage()); const pages = []; for (let i = 0; i < REVIEWS.length; i += perPage) { pages.push(REVIEWS.slice(i, i + perPage)); } const [page, setPage] = useState(0); const [trackMinH, setTrackMinH] = useState(0); const measureRef = useRef(null); // Update perPage on resize and reset to page 0 to avoid out-of-bounds useEffect(() => { const onResize = () => setPerPage(getPerPage()); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); useEffect(() => { setPage(0); }, [perPage]); // Measure the tallest review-card across ALL reviews at the current // breakpoint so the slider height never changes between pages. React.useLayoutEffect(() => { const el = measureRef.current; if (!el) return; const measure = () => { const cards = [...el.querySelectorAll('.review-card')]; let max = 0; for (let i = 0; i < cards.length; i += perPage) { const slice = cards.slice(i, i + perPage); const sliceMax = slice.reduce((m, c) => Math.max(m, c.offsetHeight), 0); max = Math.max(max, sliceMax); } if (max > 0) setTrackMinH(max); }; measure(); const ro = new ResizeObserver(measure); ro.observe(el); if (document.fonts && document.fonts.ready) document.fonts.ready.then(measure); return () => ro.disconnect(); }, [perPage]); const prev = () => setPage((x) => (x - 1 + pages.length) % pages.length); const next = () => setPage((x) => (x + 1) % pages.length); const cur = pages[page] || pages[0]; return (
{cur.map((r, i) => (
{r.quote}
{r.name}
{r.where}
via {r.via}
))} {/* pad to keep grid columns even when last page is short */} {cur.length < perPage && Array.from({ length: perPage - cur.length }).map((_, i) => ( {/* hidden measurement layer — renders every review at the current grid width so we can pick the tallest card to lock the slider height */}
); } // Real aggregate from La Signora's Google Business listing (not the // featured subset below) — keep in sync with the live Google rating. const RATING = '4.6'; const REVIEW_COUNT = '20+'; function Reviews() { return (
From her regulars

They keep
coming back.

{RATING}/ 5

From {REVIEW_COUNT} reviews on Google.

); } const OFFERINGS = [ { n: '01', badge: 'Private events', title: 'Birthdays,\nanniversaries,\nsupper clubs.', blurb: 'A full Sicilian table for 6 to 40, brought to your kitchen or your backyard. We handle the cooking and the cleanup — you pour the wine.', bullets: ['6 — 40 guests', 'Cooked on-site or pre-plated', 'Full setup & cleanup'], palette: ['#B5482B', '#8C341E', '#E0A638'], }, { n: '02', badge: 'Corporate events', title: 'Arancino stations,\nteam lunches,\nholiday parties.', blurb: 'An arancino station at your launch, a lasagna lunch for the engineering team, hand-fried cannoli for the holiday party. We bring the cart, the smile, and the food.', bullets: ['Office lunches & launches', 'Cart pop-ups on-site', 'Cannoli & espresso stations'], palette: ['#E0A638', '#B07A2D', '#5C6B33'], }, { n: '03', badge: 'Wholesale supply', title: 'Frozen trays for\ncafés, delis,\nbottle shops.', blurb: 'Frozen arancini and lasagna trays for cafés, delis, and bottle shops across the Portland-Vancouver metro. Small minimums, weekly drops.', bullets: ['Portland-Vancouver metro', 'Small minimums', 'Weekly drops'], palette: ['#5C6B33', '#3F4A21', '#E6D2A9'], }, ]; function Catering() { return (
Catering & events

Have her cook
for your table.

Out of arancini? Let la Signora come to you. Private dinners, corporate pop-ups, wholesale trays — same hands, same recipes — one kitchen (or one entire conference floor) full of people you love.

{OFFERINGS.map((o, i) => (
N° {o.n}
{o.badge}

{o.title.split('\n').map((l, k, arr) => ( {l}{k < arr.length - 1 &&
}
))}

{o.blurb}

    {o.bullets.map((b, k) =>
  • {b}
  • )}
Inquire
))}
Tell us about your event.

Two weeks' notice helps. Two days' notice has worked before.

Get in touch
); } // ── Contact ── const TOPICS = [ { v: 'catering', label: 'Catering or events' }, { v: 'wholesale', label: 'Wholesale supply' }, { v: 'restaurant', label: 'Restaurant updates' }, { v: 'media', label: 'Press / media' }, { v: 'hello', label: 'Just saying ciao' }, ]; const CHANNELS = [ { key: 'instagram', label: 'Instagram', handle: '@la_signora_wa', href: 'https://www.instagram.com/la_signora_wa/', Icon: IgIcon }, { key: 'tiktok', label: 'TikTok', handle: '@la_signora_wa', href: 'https://www.tiktok.com/@la_signora_wa', Icon: TikTokIcon }, { key: 'facebook', label: 'Facebook', handle: 'La Signora', href: 'https://www.facebook.com/LaSignoraItalianGoodFood/', Icon: FbIcon }, { key: 'email', label: 'Email', handle: 'info@lasignorafood.com', href: 'mailto:info@lasignorafood.com', Icon: MailIcon }, ]; // reCAPTCHA v2 site key — set in each page's as `window.RECAPTCHA_SITE_KEY`. // When empty the widget is skipped and the form falls back to the server-side // honeypot + rate limit (see contact-handler.php). const RECAPTCHA_SITE_KEY = (typeof window !== 'undefined' && window.RECAPTCHA_SITE_KEY) || ''; function Contact() { const [form, setForm] = useState({ topic: 'catering', name: '', email: '', message: '', company: '' }); const [errors, setErrors] = useState({}); const [sent, setSent] = useState(false); const [sending, setSending] = useState(false); const [submitError, setSubmitError] = useState(''); const recaptchaRef = useRef(null); const widgetIdRef = useRef(null); const set = (k, v) => { setForm((f) => ({ ...f, [k]: v })); setErrors((e) => ({ ...e, [k]: undefined })); }; // Render the reCAPTCHA widget once the API is ready. The container only // exists while the form is shown, so re-render after a reset (sent → false). useEffect(() => { if (sent) { widgetIdRef.current = null; return; } if (!RECAPTCHA_SITE_KEY) return; let cancelled = false; let timer; const render = () => { if (cancelled) return; const el = recaptchaRef.current; const gre = window.grecaptcha; if (gre && gre.render && el && widgetIdRef.current === null && el.childElementCount === 0) { widgetIdRef.current = gre.render(el, { sitekey: RECAPTCHA_SITE_KEY }); return; } timer = setTimeout(render, 300); }; render(); return () => { cancelled = true; clearTimeout(timer); }; }, [sent]); const resetCaptcha = () => { if (window.grecaptcha && widgetIdRef.current !== null) { window.grecaptcha.reset(widgetIdRef.current); } }; const submit = async (e) => { e.preventDefault(); const errs = {}; if (!form.name.trim()) errs.name = 'Please share your name.'; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) errs.email = 'A real email, per favore.'; if (!form.message.trim() || form.message.trim().length < 10) errs.message = 'Tell us a little more.'; setErrors(errs); if (Object.keys(errs).length > 0) return; // reCAPTCHA — require a token before we bother the server. let captchaToken = ''; if (RECAPTCHA_SITE_KEY) { captchaToken = (window.grecaptcha && widgetIdRef.current !== null) ? window.grecaptcha.getResponse(widgetIdRef.current) : ''; if (!captchaToken) { setSubmitError('Please confirm you’re human with the CAPTCHA below.'); return; } } setSending(true); setSubmitError(''); try { const payload = new URLSearchParams(form); if (captchaToken) payload.set('g-recaptcha-response', captchaToken); const res = await fetch('contact-handler.php', { method: 'POST', body: payload, }); const data = await res.json(); if (!res.ok || !data.success) throw new Error(data.error || 'send failed'); setSent(true); } catch { setSubmitError('Something went wrong sending your message. Please try again, or email info@lasignorafood.com directly.'); resetCaptcha(); } finally { setSending(false); } }; const reset = () => { setForm({ topic: 'catering', name: '', email: '', message: '', company: '' }); setErrors({}); setSent(false); setSubmitError(''); }; const topicLabel = TOPICS.find((t) => t.v === form.topic)?.label || ''; return (
Write to la Signora

Tell us
what brings
you here.

Catering, wholesale, a question about Sunday's lasagna — or maybe you just want to say ciao. Antonietta reads everything herself. (Yes, even the long ones.)

Reply within

1 — 2 days. Slower in summer market season.

Find her here
{CHANNELS.map(({ key, label, handle, href, Icon }) => { const external = key !== 'email'; return ( {label} {handle} {external && } ); })}
{!sent ? (
{TOPICS.map((t) => ( ))}
set('name', e.target.value)} placeholder="Antonietta, for example" className={errors.name ? 'invalid' : ''} autoComplete="name" /> {errors.name && {errors.name}}
set('email', e.target.value)} placeholder="your@email.com" className={errors.email ? 'invalid' : ''} autoComplete="email" /> {errors.email && {errors.email}}