Signing in...
/* @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: `
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 (Gift exchange made magical
Signing in...
Gift exchange made magical
Sign in with Google to manage your Secret Santa group and privately view your own assignment.
Gift exchange made magical
Gift exchange date
{giftExchangeDate.toLocaleString()}Track whether each gift has been purchased and log the amount spent for every participant.
Add your group first, then come back here to track spending.
Send each revealed assignment by email. Every participant with an email address will receive their recipient privately.
All assignment emails were sent successfully.
Some emails could not be sent: