Seating Chart
Drag guest chips onto tables to assign seats for the reception.
/* @component-map * App — Main container, section navigation [app.jsx] * HeroSection — Wedding hero banner with couple names and date [components/HeroSection.jsx] * EventDetails — Event timeline, venue info, and photos [components/EventDetails.jsx] * RSVPForm — RSVP submission form with meal choice, dietary, plus one [components/RSVPForm.jsx] * RSVPStats — RSVP count and meal choice breakdown [components/RSVPStats.jsx] * @end-component-map */ // DEPLOY_CONFIG: {"triggers": [{"name": "venue-capacity-reached", "on": "collection.add", "collection": "rsvps", "actions": [{"type": "email", "to": "couple", "subject": "Venue capacity reached", "body": "RSVP count has reached venue capacity. Please review and manage the guest list."}]}]} import { useEffect, useMemo, useState } from 'react'; import { useCollection, useAuth } from '@deplixo/sdk'; import { HeroSection } from './components/HeroSection.jsx'; import { EventDetails } from './components/EventDetails.jsx'; import { RSVPForm } from './components/RSVPForm.jsx'; import { RSVPStats } from './components/RSVPStats.jsx'; const ADMIN_EMAILS = [ 'emma@example.com', 'james@example.com', ]; const SEATING_COLLECTION = 'wedding_seating_assignments'; const DEFAULT_TABLES = [ { id: 'table-1', name: 'Table 1', seats: 8, x: 16, y: 16 }, { id: 'table-2', name: 'Table 2', seats: 8, x: 52, y: 16 }, { id: 'table-3', name: 'Table 3', seats: 8, x: 16, y: 48 }, { id: 'table-4', name: 'Table 4', seats: 8, x: 52, y: 48 }, { id: 'head', name: 'Head Table', seats: 10, x: 34, y: 80 }, ]; function App() { const { user, loading, login, logout } = useAuth(); const [activeSection, setActiveSection] = useState('home'); const [draggingGuestId, setDraggingGuestId] = useState(null); const [tableAssignments, setTableAssignments] = useState({}); const [guests, setGuests] = useState([]); const [lastDroppedGuest, setLastDroppedGuest] = useState(null); const { items: seatingItems, loading: seatingLoading, add: addAssignment, update: updateAssignment } = useCollection(SEATING_COLLECTION, { personal: true }); const isAdmin = !!user && ADMIN_EMAILS.includes((user.email || '').toLowerCase()); const canAccessAdmin = isAdmin; const sections = [ { id: 'home', label: '💒 Home' }, { id: 'details', label: '📋 Details' }, { id: 'rsvp', label: '💌 RSVP' }, { id: 'stats', label: '📊 Guests', adminOnly: true }, { id: 'seating', label: '🪑 Seating', adminOnly: true }, ]; useEffect(() => { const normalized = Array.isArray(seatingItems) ? seatingItems : []; const nextAssignments = {}; const nextGuests = []; normalized.forEach((item) => { const guestId = item.guestId || item.id; const guestName = item.guestName || item.name || `Guest ${guestId}`; nextAssignments[guestId] = item.tableId || null; nextGuests.push({ id: guestId, name: guestName, tableId: item.tableId || null, }); }); setTableAssignments(nextAssignments); setGuests(nextGuests); }, [seatingItems]); const tableCounts = useMemo(() => { return DEFAULT_TABLES.reduce((acc, table) => { acc[table.id] = guests.filter((g) => g.tableId === table.id).length; return acc; }, {}); }, [guests]); const unassignedGuests = guests.filter((guest) => !guest.tableId); const handleSectionChange = (sectionId) => { if (sectionId === 'stats' && !canAccessAdmin) return; if (sectionId === 'seating' && !canAccessAdmin) return; setActiveSection(sectionId); }; const persistAssignment = async (guest) => { const existing = Array.isArray(seatingItems) ? seatingItems.find((item) => (item.guestId || item.id) === guest.id) : null; const payload = { guestId: guest.id, guestName: guest.name, tableId: guest.tableId || null, }; if (existing?.id) { await updateAssignment(existing.id, payload); } else { await addAssignment(payload); } }; const handleAssignToTable = async (guestId, tableId) => { const guest = guests.find((g) => g.id === guestId); if (!guest) return; const nextGuest = { ...guest, tableId }; const nextGuests = guests.map((g) => (g.id === guestId ? nextGuest : g)); setGuests(nextGuests); setTableAssignments((prev) => ({ ...prev, [guestId]: tableId })); setLastDroppedGuest(guest.name); try { await persistAssignment(nextGuest); } catch (e) { setGuests(guests); setTableAssignments((prev) => ({ ...prev, [guestId]: guest.tableId || null })); } }; const handleClearAssignment = async (guestId) => { const guest = guests.find((g) => g.id === guestId); if (!guest) return; const nextGuest = { ...guest, tableId: null }; setGuests((prev) => prev.map((g) => (g.id === guestId ? nextGuest : g))); setTableAssignments((prev) => ({ ...prev, [guestId]: null })); try { await persistAssignment(nextGuest); } catch (e) { setGuests((prev) => prev.map((g) => (g.id === guestId ? guest : g))); setTableAssignments((prev) => ({ ...prev, [guestId]: guest.tableId || null })); } }; const handleDropOnTable = (guestId, tableId) => { if (!guestId) return; handleAssignToTable(guestId, tableId); setDraggingGuestId(null); }; if (loading) return
Google sign-in is required to access the couple's admin view.
You're signed in as {user.name || user.email}, but this admin view is only available to authorized accounts.
Drag guest chips onto tables to assign seats for the reception.