// 432 Bleu — Box Office · ticket drawer (tier select, contact info, real checkout) const { useState, useEffect } = React; function TicketDrawer({ show, accent, onClose }) { const [selectedTierId, setSelectedTierId] = useState(null); const [pwycAmount, setPwycAmount] = useState(''); const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [submitting, setSubmitting] = useState(false); const [cryptoSubmitting, setCryptoSubmitting] = useState(false); const [error, setError] = useState(''); const [done, setDone] = useState(false); const [attempted, setAttempted] = useState(false); useEffect(() => { setSelectedTierId(null); setPwycAmount(''); setName(''); setEmail(''); setSubmitting(false); setCryptoSubmitting(false); setError(''); setDone(false); setAttempted(false); }, [show && show.id]); // lock scroll while open useEffect(() => { if (show) { document.body.style.overflow = 'hidden'; } return () => { document.body.style.overflow = ''; }; }, [show]); const open = !!show; const selectedTier = show && show.tiers.find(t => t.id === selectedTierId); const isPwyc = !!(selectedTier && selectedTier.name === 'PWYC'); const amountCents = !selectedTier ? 0 : isPwyc ? Math.round((parseFloat(pwycAmount) || 0) * 100) : selectedTier.priceCents; const canSubmit = !!selectedTier && name.trim() !== '' && email.includes('@') && !submitting && !(isPwyc && amountCents < 100); const canSubmitCrypto = canSubmit && selectedTier && selectedTier.name !== 'GA' && !cryptoSubmitting; const allSoldOut = !!show && show.tiers.length > 0 && show.tiers.every(t => !t.available); const nameInvalid = attempted && name.trim() === ''; const emailInvalid = attempted && !email.includes('@'); async function handleConfirm() { if (!canSubmit) { setAttempted(true); return; } setSubmitting(true); setError(''); try { if (amountCents === 0 && !isPwyc) { const res = await fetch('/api/tickets/free', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event_id: show.id, tier_id: selectedTier.id, email: email.trim(), name: name.trim(), }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || 'Something went wrong. Please try again.'); } setDone(true); setSubmitting(false); } else { const res = await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event_id: show.id, tier_id: selectedTier.id, email: email.trim(), name: name.trim(), amount_cents: amountCents, }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || 'Something went wrong. Please try again.'); } const { checkout_url } = await res.json(); window.location.href = checkout_url; // leaving the SPA for Stripe } } catch (e) { setError(e.message || 'Something went wrong. Please try again.'); setSubmitting(false); } } async function handleCryptoConfirm() { if (!canSubmitCrypto) { setAttempted(true); return; } setCryptoSubmitting(true); setError(''); try { const res = await fetch('/api/checkout/crypto', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event_id: show.id, tier_id: selectedTier.id, email: email.trim(), name: name.trim(), amount_cents: amountCents, }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || 'Crypto checkout is unavailable right now.'); } const { checkout_url } = await res.json(); window.location.href = checkout_url; // leaving the SPA for NOWPayments } catch (e) { setError(e.message || 'Crypto checkout is unavailable right now.'); setCryptoSubmitting(false); } } return ( <> {/* scrim */}
{/* panel */} ); } function Header({ show, accent, onClose }) { return (
SELECT ADMISSION
{show.artist}
{show.dateLabel} {show.doorsLabel && DOORS {show.doorsLabel}} SHOW {show.timeLabel} {show.room}
); } function TierRow({ tier, accent, selected, onSelect, pwycAmount, onPwycAmount }) { const isPwyc = tier.name === 'PWYC'; const remaining = tier.capacity != null ? Math.max(0, tier.capacity - tier.sold) : null; return (
{tier.label}{!tier.available ? ' — SOLD OUT' : ''} {formatPrice(tier)}
{tier.description &&
{tier.description}
} {remaining !== null && tier.available &&
{remaining} remaining
} {isPwyc && selected && (
e.stopPropagation()} style={{ marginTop: 14, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}> YOUR AMOUNT
$ onPwycAmount(e.target.value)} style={{ width: 70, background: 'transparent', border: 'none', outline: 'none', color: '#fff', fontFamily: '"JetBrains Mono", monospace', fontSize: 15, padding: '8px 8px' }} />
$1 minimum · tip the room
)}
); } function ContactForm({ accent, name, email, onName, onEmail, nameInvalid, emailInvalid }) { const field = (label, value, onChange, type, placeholder, autoComplete, invalid) => (
onChange(e.target.value)} style={{ width: '100%', background: invalid ? 'rgba(255,107,107,0.08)' : 'rgba(255,255,255,0.04)', border: `1px solid ${invalid ? '#ff6b6b' : 'rgba(95,230,221,0.2)'}`, color: '#fff', fontFamily: '"Space Grotesk", sans-serif', fontSize: 14, padding: '12px 14px', outline: 'none', boxSizing: 'border-box', transition: 'border-color 0.2s ease, background 0.2s ease' }} />
); return (
YOUR INFO
{field('NAME', name, onName, 'text', 'Your name', 'name', nameInvalid)} {field('EMAIL', email, onEmail, 'email', 'ticket@example.com', 'email', emailInvalid)}
); } function Footer({ accent, tier, amountCents, canSubmit, submitting, error, onConfirm, canSubmitCrypto, cryptoSubmitting, onCryptoConfirm }) { const isPwyc = !!(tier && tier.name === 'PWYC'); const priceLabel = !tier ? '—' : amountCents === 0 ? (isPwyc ? '—' : 'FREE') : '$' + (amountCents / 100).toFixed(2); const btnLabel = submitting ? 'PROCESSING…' : !tier ? 'SELECT A TICKET' : isPwyc && amountCents < 100 ? 'ENTER AN AMOUNT' : amountCents === 0 ? 'CLAIM FREE TICKET' : 'SECURE TICKETS'; const showCryptoBtn = !!tier && tier.name !== 'GA'; const cryptoBtnLabel = cryptoSubmitting ? 'REDIRECTING…' : 'PAY WITH CRYPTO'; return (
{tier ? tier.label : 'NO TICKET SELECTED'} {priceLabel}
{showCryptoBtn && ( )} {error &&
{error}
}
ENCRYPTED · NO FEES AT THE DOOR
); } function Confirmation({ show, accent, tier, amountCents, email, onClose }) { const priceLabel = amountCents === 0 ? 'FREE' : '$' + (amountCents / 100).toFixed(2); return (
RESONANCE LOCKED
You're on the list.
{tier.label} · {priceLabel} for {show.artist}
{show.dateLabel} · {show.doorsLabel ? `DOORS ${show.doorsLabel} · SHOW ` : ''}{show.timeLabel} · {show.room}
YOUR TICKET CODE
On its way to {email}. Check your inbox and bring the code to the door.

Tuned to {show.hz} Hz. All entries 21+.

); } Object.assign(window, { TicketDrawer });