/* @component-map * App — Main container, routes between setup and main app views * SetupScreen — Initial setup flow for user identity and reference date * MainApp — Primary app view with seat display, actions, holidays * Header — App title and theme button * BirthdayBanner — Shows birthday celebration banner on Sept 18 * SeatCard — Displays who has front seat today with streak and swap info * ActionButtons — Swap and dispute buttons * BirthdayCountdown — Mini card showing days until birthday * HolidaySection — Upcoming holidays list with add custom holiday * ThemeModal — Theme picker modal * DisputeModal — Dispute declaration modal with logic explanation * AddHolidayModal — Modal to add custom holidays * @end-component-map */ import { useCollection } from '@deplixo/sdk'; const THEMES = [ { id: 'theme-pastel', label: '🌸 Blossom', bg: '#fff0f6', accent: '#ff6b9d', text: '#2d1b2e' }, { id: 'theme-retro', label: '👾 Retro', bg: '#0a0a1a', accent: '#00ff9f', text: '#ffffff' }, { id: 'theme-luxury', label: '✨ Luxe', bg: '#1a1408', accent: '#d4af37', text: '#f5f0e8' }, { id: 'theme-nature', label: '🌿 Nature', bg: '#e8f5e9', accent: '#2e7d32', text: '#1b3a1c' }, { id: 'theme-neon', label: '⚡ Neon', bg: '#0d0d0d', accent: '#ff073a', text: '#ffffff' }, { id: 'theme-lemon', label: '🍋 Lemon', bg: '#fffde7', accent: '#f9a825', text: '#3e2a00' }, { id: 'theme-sky', label: '🩵 Sky', bg: '#e3f2fd', accent: '#1976d2', text: '#0d1b2a' }, { id: 'theme-peach', label: '🍑 Peach', bg: '#fff3e0', accent: '#e64a19', text: '#3e1000' }, { id: 'theme-lavender', label: '💜 Lavender', bg: '#f3e5f5', accent: '#7b1fa2', text: '#1a0030' }, { id: 'theme-mint', label: '🌿 Mint', bg: '#e0f7fa', accent: '#00838f', text: '#00252a' }, ]; const US_HOLIDAYS_STATIC = [ { name: "New Year's Day", month: 1, day: 1 }, { name: "Valentine's Day", month: 2, day: 14 }, { name: "St. Patrick's Day", month: 3, day: 17 }, { name: "Easter", dynamic: true, fnName: 'getEaster' }, { name: "Mother's Day", dynamic: true, fnName: 'getMothersDay' }, { name: "Memorial Day", dynamic: true, fnName: 'getMemorialDay' }, { name: "Father's Day", dynamic: true, fnName: 'getFathersDay' }, { name: "Independence Day", month: 7, day: 4 }, { name: "Labor Day", dynamic: true, fnName: 'getLaborDay' }, { name: "Halloween", month: 10, day: 31 }, { name: "Thanksgiving", dynamic: true, fnName: 'getThanksgiving' }, { name: "Christmas", month: 12, day: 25 }, { name: "New Year's Eve", month: 12, day: 31 }, ]; function getEaster(year) { const a = year % 19, b = Math.floor(year / 100), c = year % 100; const d = Math.floor(b / 4), e = b % 4, f = Math.floor((b + 8) / 25); const g = Math.floor((b - f + 1) / 3), h = (19 * a + b - d - g + 15) % 30; const i = Math.floor(c / 4), k = c % 4, l = (32 + 2 * e + 2 * i - h - k) % 7; const m = Math.floor((a + 11 * h + 22 * l) / 451); const month = Math.floor((h + l - 7 * m + 114) / 31); const day = ((h + l - 7 * m + 114) % 31) + 1; return new Date(year, month - 1, day); } function getNthWeekday(year, month, weekday, n) { const d = new Date(year, month - 1, 1); let count = 0; while (true) { if (d.getDay() === weekday) { count++; if (count === n) return new Date(d); } d.setDate(d.getDate() + 1); if (d.getMonth() !== month - 1) break; } return null; } function getLastWeekday(year, month, weekday) { const d = new Date(year, month, 0); while (d.getDay() !== weekday) d.setDate(d.getDate() - 1); return d; } function getMothersDay(y) { return getNthWeekday(y, 5, 0, 2); } function getMemorialDay(y) { return getLastWeekday(y, 5, 1); } function getFathersDay(y) { return getNthWeekday(y, 6, 0, 3); } function getLaborDay(y) { return getNthWeekday(y, 9, 1, 1); } function getThanksgiving(y) { return getNthWeekday(y, 11, 4, 4); } const dynamicFns = { getEaster, getMothersDay, getMemorialDay, getFathersDay, getLaborDay, getThanksgiving }; function getToday() { const d = new Date(); return new Date(d.getFullYear(), d.getMonth(), d.getDate()); } function dateToStr(d) { return d.toISOString().split('T')[0]; } function daysBetween(a, b) { return Math.round((b - a) / (1000 * 60 * 60 * 24)); } function whoHasSeatOnDate(refDate, refPerson, date) { const ref = new Date(refDate); const diff = daysBetween(ref, date); const isEven = diff % 2 === 0; if (isEven) return refPerson; return refPerson === 'Madelyn' ? 'Sydney' : 'Madelyn'; } function getEffectiveSeat(refDate, refPerson, swapStartDate, date) { const base = whoHasSeatOnDate(refDate, refPerson, date); if (swapStartDate && date >= new Date(swapStartDate)) { return base === 'Madelyn' ? 'Sydney' : 'Madelyn'; } return base; } function getBirthdayCountdown() { const t = getToday(); let bday = new Date(t.getFullYear(), 8, 18); if (bday <= t) bday = new Date(t.getFullYear() + 1, 8, 18); return daysBetween(t, bday); } function getAllHolidays(customHolidays) { const t = getToday(); const year = t.getFullYear(); const holidays = []; for (const h of US_HOLIDAYS_STATIC) { for (let y = year; y <= year + 1; y++) { let d = h.dynamic ? dynamicFns[h.fnName](y) : new Date(y, h.month - 1, h.day); if (d && d >= t) { holidays.push({ name: h.name, date: d }); break; } } } for (const ch of (customHolidays || [])) { const val = ch.value || ch; let d = new Date(val.date + 'T00:00:00'); if (val.repeat === 'annual') { let candidate = new Date(year, d.getMonth(), d.getDate()); if (candidate < t) candidate = new Date(year + 1, d.getMonth(), d.getDate()); holidays.push({ name: val.name, date: candidate, custom: true, id: ch.id }); } else { if (d >= t) holidays.push({ name: val.name, date: d, custom: true, id: ch.id }); } } holidays.sort((a, b) => a.date - b.date); return holidays; } function SetupScreen({ onFinish }) { const [setupUser, setSetupUser] = React.useState(null); const [setupRefPerson, setSetupRefPerson] = React.useState(null); const [setupDate, setSetupDate] = React.useState(''); const handleFinish = () => { if (!setupUser || !setupRefPerson || !setupDate) { alert('Please fill everything in!'); return; } onFinish({ user: setupUser, refDate: setupDate, refPerson: setupRefPerson, streakStart: dateToStr(getToday()), swapStartDate: null, theme: 'theme-pastel', }); }; return (
🚗 Shotgun!

Let's get you set up

setSetupUser('Madelyn')} >Madelyn
setSetupUser('Sydney')} >Sydney
setSetupDate(e.target.value)} />
setSetupRefPerson('Madelyn')} >Madelyn
setSetupRefPerson('Sydney')} >Sydney
); } function BirthdayBanner() { const t = getToday(); const isBirthday = t.getMonth() === 8 && t.getDate() === 18; if (!isBirthday) return null; return (
🎉

Happy Birthday Madelyn & Sydney! 🎂

Today is YOUR day — both of you!

🎊
); } function Header({ theme, onOpenTheme }) { return (
🚗 Shotgun!
); } function SeatCard({ state }) { const t = getToday(); const todayStr = dateToStr(t); const seat = getEffectiveSeat(state.refDate, state.refPerson, state.swapStartDate, t); const isMe = seat === state.user; const tmr = new Date(t); tmr.setDate(tmr.getDate() + 1); const tomorrowSeat = getEffectiveSeat(state.refDate, state.refPerson, state.swapStartDate, tmr); const streakDays = Math.max(0, daysBetween(new Date(state.streakStart), t)); const hasSwap = !!state.swapStartDate; const swapDate = hasSwap ? new Date(state.swapStartDate) : null; return (
Front seat today
{isMe ? `${seat} 🙋` : seat}
{isMe ? "That's YOU today!" : `${seat} has shotgun`}
Tomorrow: {tomorrowSeat}
🔥 Streak: {streakDays} days no disputes
{hasSwap && (
🔄 Schedule swapped from {swapDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} onward
)}
); } function ActionButtons({ state, onSwap, onOpenDispute }) { const todayStr = dateToStr(getToday()); const isSwapToday = state.swapStartDate && dateToStr(new Date(state.swapStartDate)) === todayStr; return (
); } function BirthdayCountdown() { const t = getToday(); const isBirthday = t.getMonth() === 8 && t.getDate() === 18; return (
🎂 Birthday Countdown
September 18th
{isBirthday ? '🎂 TODAY!' : `${getBirthdayCountdown()} days`}
); } function HolidaySection({ customHolidays, onDeleteHoliday, onOpenAddHoliday }) { const holidays = getAllHolidays(customHolidays); const t = getToday(); const next = holidays.length > 0 ? holidays[0] : null; const upcoming = holidays.slice(1, 6); return (
🗓️ Upcoming Holidays
{next ? (
{next.name}
{daysBetween(t, next.date) === 0 ? '🎉 TODAY!' : `${daysBetween(t, next.date)} days away`}
{next.date.toLocaleDateString('en-US', { month: 'long', day: 'numeric' })}
) : (
No upcoming holidays
)} {upcoming.length > 0 && ( )}
); } function ThemeModal({ currentTheme, onSelectTheme, onClose }) { return (
{ if (e.target === e.currentTarget) onClose(); }}>

🎨 Choose Theme

{THEMES.map(t => (
onSelectTheme(t.id)} > {t.label}
))}
); } function DisputeModal({ state, onConfirm, onClose }) { const t = getToday(); const base = whoHasSeatOnDate(state.refDate, state.refPerson, t); const ref = new Date(state.refDate); const diff = daysBetween(ref, t); const hasSwap = state.swapStartDate && t >= new Date(state.swapStartDate); return (
{ if (e.target === e.currentTarget) onClose(); }}>

⚡ Declare Dispute

This will reset your streak to 0. The math behind today's seat assignment is shown below so you can settle it together.

Starting from {state.refDate} when {state.refPerson} had the seat, today is day {diff} ({diff % 2 === 0 ? 'even' : 'odd'}) from that reference. By alternating, the base seat belongs to {base} today. {hasSwap && (
⚠️ A schedule swap is active from {state.swapStartDate}, flipping the seat.
)}

After declaring, ask the other person to tap Dispute on their phone too to reset their streak.

); } function AddHolidayModal({ onSave, onClose }) { const [name, setName] = React.useState(''); const [date, setDate] = React.useState(''); const [repeat, setRepeat] = React.useState('annual'); const handleSave = () => { if (!name.trim() || !date) { alert('Please fill in the name and date.'); return; } onSave({ name: name.trim(), date, repeat }); }; return (
{ if (e.target === e.currentTarget) onClose(); }}>

🗓️ Add Holiday

setName(e.target.value)} /> setDate(e.target.value)} />
); } function MainApp({ state, stateId, onUpdateState, customHolidays, onAddHoliday, onDeleteHoliday }) { const [showThemeModal, setShowThemeModal] = React.useState(false); const [showDisputeModal, setShowDisputeModal] = React.useState(false); const [showHolidayModal, setShowHolidayModal] = React.useState(false); const handleSwap = () => { const todayStr = dateToStr(getToday()); if (state.swapStartDate && dateToStr(new Date(state.swapStartDate)) === todayStr) { onUpdateState({ ...state, swapStartDate: null }); } else { onUpdateState({ ...state, swapStartDate: todayStr }); } }; const handleSelectTheme = (themeId) => { onUpdateState({ ...state, theme: themeId }); }; const handleConfirmDispute = () => { onUpdateState({ ...state, streakStart: dateToStr(getToday()) }); setShowDisputeModal(false); setTimeout(() => alert('Streak reset! Remind the other person to tap Dispute on their phone too.'), 100); }; const handleSaveHoliday = (holiday) => { onAddHoliday(holiday); setShowHolidayModal(false); }; React.useEffect(() => { document.body.className = state.theme || 'theme-pastel'; }, [state.theme]); return (
setShowThemeModal(true)} /> setShowDisputeModal(true)} /> setShowHolidayModal(true)} />
{showThemeModal && ( setShowThemeModal(false)} /> )} {showDisputeModal && ( setShowDisputeModal(false)} /> )} {showHolidayModal && ( setShowHolidayModal(false)} /> )}
); } function App() { const { items: stateItems, loading: stateLoading, add: addState, update: updateState, collection: stateCollection } = useCollection('shotgun_state', { personal: true }); const { items: holidayItems, loading: holidaysLoading, add: addHoliday, remove: removeHoliday } = useCollection('shotgun_holidays', { personal: true }); const currentState = stateItems.length > 0 ? stateItems[0] : null; const handleFinishSetup = async (data) => { await addState(data); }; const handleUpdateState = async (newData) => { if (currentState) { await updateState(currentState.id, newData); } }; const handleAddHoliday = async (holiday) => { await addHoliday(holiday); }; const handleDeleteHoliday = async (id) => { await removeHoliday(id); }; if (stateLoading || holidaysLoading) { return (
🚗 Shotgun!

Loading...

); } if (!currentState) { return ; } const stateValue = currentState.value || currentState; return ( ); } ReactDOM.createRoot(document.getElementById("root")).render();