/* @component-map * App — Main container, tab navigation [app.jsx] * GroupSetup — Create group and add participants [components/GroupSetup.jsx] * Assignments — Draw names and reveal assignments [components/Assignments.jsx] * Wishlists — Manage wishlists for participants [components/Wishlists.jsx] * @end-component-map */ // DEPLOY_CONFIG: {"triggers": [{"name": "notify_organizer_when_all_gifts_purchased", "on": "collection.update", "collection": "gifts", "actions": [{"type": "email", "to": "{{organizer_email}}", "subject": "All gifts have been purchased", "body": "All gifts in the registry have now been marked as purchased. Please check the app for the latest updates."}]}]} import { useEffect, useMemo, useState } from 'react'; import { useAuth, useCollection, sendEmail } from '@deplixo/sdk'; import { GroupSetup } from './components/GroupSetup.jsx'; import { Assignments } from './components/Assignments.jsx'; import { Wishlists } from './components/Wishlists.jsx'; function App() { const { user, loading, login, logout } = useAuth(); const [activeTab, setActiveTab] = useState('group'); const [now, setNow] = useState(Date.now()); const participants = useCollection('participants', { personal: true }); const assignments = useCollection('assignments', { personal: true }); const wishlists = useCollection('wishlists', { personal: true }); const budgetTracker = useCollection('budgetTracker', { personal: true }); const groupSettings = useCollection('groupSettings', { personal: true }); const participantsItems = participants.items || []; const assignmentsItems = assignments.items || []; const budgetItems = budgetTracker.items || []; const groupSettingsItems = groupSettings.items || []; const [emailStatus, setEmailStatus] = useState({ sending: false, sentCount: 0, total: 0, errors: [], success: false, }); useEffect(() => { const timer = window.setInterval(() => setNow(Date.now()), 1000); return () => window.clearInterval(timer); }, []); const tabs = [ { id: 'group', label: '🎄 Group', icon: '👥' }, { id: 'assignments', label: '🎁 Draw', icon: '🎲' }, { id: 'wishlists', label: '📝 Wishlists', icon: '⭐' }, { id: 'budget', label: '💰 Budget', icon: '📊' }, ]; const budgetByParticipantId = useMemo(() => { return budgetItems.reduce((map, entry) => { map[entry.participantId] = entry; return map; }, {}); }, [budgetItems]); const participantById = useMemo(() => { return participantsItems.reduce((map, participant) => { map[participant.id] = participant; return map; }, {}); }, [participantsItems]); const assignmentRecipients = useMemo(() => { return assignmentsItems .map(assignment => { const giver = participantById[assignment.giverId]; const receiver = participantById[assignment.receiverId]; return giver && receiver ? { assignment, giver, receiver } : null; }) .filter(Boolean); }, [assignmentsItems, participantById]); const giftExchangeDate = useMemo(() => { const candidates = [ groupSettingsItems[0]?.giftExchangeDate, groupSettingsItems[0]?.exchangeDate, groupSettingsItems[0]?.date, ].filter(Boolean); const rawValue = candidates[0]; if (!rawValue) return null; const parsed = new Date(rawValue); return Number.isNaN(parsed.getTime()) ? null : parsed; }, [groupSettingsItems]); const countdown = useMemo(() => { if (!giftExchangeDate) return null; const diff = giftExchangeDate.getTime() - now; const totalSeconds = Math.max(0, Math.floor(diff / 1000)); const days = Math.floor(totalSeconds / 86400); const hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; return { isPast: diff <= 0, days, hours, minutes, seconds, display: diff <= 0 ? '🎉 Gift exchange day is here!' : `${days}d ${hours}h ${minutes}m ${seconds}s`, }; }, [giftExchangeDate, now]); const upsertBudgetRecord = async (participantId, updates) => { const existing = budgetByParticipantId[participantId]; if (existing?.id) { await budgetTracker.update(existing.id, { ...existing, ...updates, participantId, }); } else { await budgetTracker.add({ participantId, purchased: false, amount: '', ...updates, }); } }; const sendAssignmentEmails = async () => { setEmailStatus({ sending: true, sentCount: 0, total: assignmentRecipients.length, errors: [], success: false }); const errors = []; let sentCount = 0; for (const item of assignmentRecipients) { const recipientEmail = item.giver.email || item.giver?.emailAddress || ''; if (!recipientEmail) { errors.push(`Missing email for ${item.giver.name || 'a participant'}`); continue; } try { await sendEmail({ to: recipientEmail, subject: 'Your Secret Santa assignment', html: `

Hi ${item.giver.name || 'there'}!

Your Secret Santa recipient is ${item.receiver.name || 'someone'}.

Happy gifting! 🎁

`, }); sentCount += 1; setEmailStatus(prev => ({ ...prev, sentCount })); } catch (error) { errors.push(`${item.giver.name || recipientEmail}: ${error?.message || 'Failed to send email'}`); } } setEmailStatus({ sending: false, sentCount, total: assignmentRecipients.length, errors, success: errors.length === 0 && sentCount > 0, }); }; if (loading) { return (
🎅

Secret Santa

Gift exchange made magical

Signing in...

); } if (!user) { return (
🎅

Secret Santa

Gift exchange made magical

Sign in to continue

Sign in with Google to manage your Secret Santa group and privately view your own assignment.

); } return (
🎅

Secret Santa

Gift exchange made magical

{user.avatar ? ( {user.name} ) : ( {user.name?.[0] || 'U'} )}
{user.name} {user.email}
{giftExchangeDate && (

Gift exchange date

{giftExchangeDate.toLocaleString()}
{countdown?.display} {countdown?.isPast ? 'Time to exchange gifts!' : 'Remaining'}
)} {activeTab === 'group' && ( )} {activeTab === 'assignments' && ( )} {activeTab === 'wishlists' && ( )} {activeTab === 'budget' && (

Budget Tracker

{participantsItems.length} people

Track whether each gift has been purchased and log the amount spent for every participant.

{participantsItems.length === 0 ? (
🎁

No participants yet

Add your group first, then come back here to track spending.

) : (
    {participantsItems.map(participant => { const record = budgetByParticipantId[participant.id] || {}; const purchased = !!record.purchased; const amountValue = record.amount ?? ''; return (
  • {(participant.name || 'U')[0]?.toUpperCase()}
    {participant.name} {participant.email || 'No email added'}
    {purchased ? 'Purchased' : 'Not purchased'}
    upsertBudgetRecord(participant.id, { purchased, amount: e.target.value, })} />
  • ); })}
)}
)} {activeTab === 'assignments' && (

Assignment Emails

{assignmentRecipients.length} ready

Send each revealed assignment by email. Every participant with an email address will receive their recipient privately.

{emailStatus.sentCount}/{emailStatus.total} sent
{emailStatus.success && (

All assignment emails were sent successfully.

)} {emailStatus.errors.length > 0 && (

Some emails could not be sent:

    {emailStatus.errors.map((error, index) => (
  • {error}
  • ))}
)}
)}
); } ReactDOM.createRoot(document.getElementById('root')).render();