// app.jsx — RoCo Arts Council site shell // Mounts the React app, wires the hash router to page components, // and provides global utilities (toasts, newsletter, push notifications). // ─── Toast system ───────────────────────────────────────────────── // Accessible from anywhere via window.showToast(message, type, duration). // Types: 'info' (default) | 'success' | 'error' let _toastId = 0; const _toastListeners = new Set(); function showToast(message, type = 'info', duration = 4500) { const id = ++_toastId; _toastListeners.forEach(fn => fn({ id, message, type, duration })); return id; } function ToastManager() { const [toasts, setToasts] = React.useState([]); React.useEffect(() => { const listener = (toast) => { setToasts(prev => [...prev, toast]); setTimeout(() => { setToasts(prev => prev.filter(t => t.id !== toast.id)); }, toast.duration); }; _toastListeners.add(listener); return () => _toastListeners.delete(listener); }, []); const dismiss = (id) => setToasts(prev => prev.filter(t => t.id !== id)); const root = document.getElementById('roco-toasts'); if (!root || !toasts.length) return null; const icons = { success: '✓', error: '✕', info: '◉' }; return ReactDOM.createPortal( <> {toasts.map(t => (
{icons[t.type] || icons.info} {t.message}
))} , root ); } // ─── Newsletter inline form ──────────────────────────────────────── // Used in the news strip and footer. Calls API.subscribeNewsletter(). function NewsletterForm({ buttonText = 'Subscribe', dark = true }) { const [email, setEmail] = React.useState(''); const [status, setStatus] = React.useState('idle'); // idle | loading | done | error const submit = async (e) => { e.preventDefault(); if (!email || status === 'loading') return; setStatus('loading'); try { await API.subscribeNewsletter(email); setStatus('done'); setEmail(''); showToast("You're on the list. One email a month, no spam.", 'success'); } catch { setStatus('error'); showToast('Something went wrong — try again or email us directly.', 'error'); } }; if (status === 'done') { return ( ✓ You're subscribed. ); } return (
setEmail(e.target.value)} placeholder="your@email.com" style={{ flex: '1 1 180px', padding: '10px 14px', borderRadius: 6, border: `1.5px solid ${dark ? 'rgba(251,247,238,0.25)' : '#E8E2D3'}`, background: dark ? 'rgba(251,247,238,0.08)' : '#fff', color: dark ? '#FBF7EE' : '#1F1B2E', fontSize: 14, fontFamily: 'inherit', outline: 'none', }} />
); } // ─── Placeholder pages ──────────────────────────────────────────── function PageNotFound() { return (
404

Page not found

This page doesn't exist. Maybe it moved, or maybe it's still being built.

← Back to home
); } function PageComingSoon({ title, description }) { return (
Coming soon

{title}

{description && (

{description}

)} ← Back to home
); } // ─── Page router ────────────────────────────────────────────────── function PageRouter() { const { page, id } = useCurrentPage(); switch (page) { case 'home': return ; case 'artists': return ; case 'artist-profile': // In production: fetch artist by slug from WordPress, then render ProfileArtist. // For now, demo both profile types by slug. if (id === 'jacob-steinmiller') return ; return ; case 'events': return ; // list view is the primary events index case 'event-detail': // Route by id/slug to the correct template. // Drop-in / multi-site Open Studios campaign if (id === 'spring-open-studios' || id === 'spring-os' || id === 'fall-os') return ; // Gallery opening / exhibit if (id === 'ss-open' || (id && (id.includes('senior') || id.includes('exhibit') || id.includes('gallery') || id.includes('opening')))) { return ; } // Default: class or concert return ; case 'news': return ; case 'news-article': { // Open calls and announcements use the sidebar template const announcementIds = ['qw-2027', 'draw-series', 'hours-summer', 'board-spring', 'summer-call', 'students-25']; if (id && (announcementIds.includes(id) || id.includes('call') || id.includes('apply') || id.includes('grant'))) { return ; } return ; } case 'about': return ; case 'contact': return ; default: return ; } } // ─── App wrapper ────────────────────────────────────────────────── function AppContent() { const { path } = useRouter(); // Signal loading complete on first mount React.useEffect(() => { window.dispatchEvent(new CustomEvent('roco:ready')); }, []); // Re-signal on route changes so analytics can track page views React.useEffect(() => { window.dispatchEvent(new CustomEvent('roco:navigate', { detail: { path } })); }, [path]); return (
); } function App() { return ( ); } // Expose utilities globally so non-React code (push banner buttons, etc.) can use them Object.assign(window, { showToast, NewsletterForm }); ReactDOM.createRoot(document.getElementById('root')).render();