import type { Models } from "appwrite"; import { Activity, AlertTriangle, CalendarDays, CheckCircle2, ChevronRight, Cloud, Command, Database, Edit3, FileJson, FileSpreadsheet, Gauge, Github, Home, LineChart, Loader2, LogIn, LogOut, Plus, PoundSterling, RefreshCcw, RotateCcw, Search, Settings2, ShieldCheck, TimerReset, Trash2, Upload, User, X, Zap, } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type FormEvent, type ReactNode, } from "react"; import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Line, LineChart as RechartsLineChart, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { BUILT_IN_FLAVOURS, DEFAULT_FLAVOUR, accentForCustomFlavour, flavourMeta, mergedFlavours } from "./data/flavours"; import { account, appwriteConfig, Channel, client, OAuthProvider, pingAppwrite } from "./lib/appwrite"; import { appwriteErrorMessage, createEntries, createEntry, deleteEntry as deleteEntryDocument, isDuplicateDraft, listEntries, updateEntry, } from "./lib/appwriteEntries"; import { createExcelExport, downloadBlob, parseExcelImport } from "./lib/excel"; import { caffeineFor, caffeinePerCan, currency, currentStreak, daysSinceLast, defaultPriceForSize, entriesInRange, formatLocalInput, groupByDay, groupByFlavour, groupByWeek, highestAveragePrice, humanDateTime, makeId, oneDecimal, spendFor, startOfDay, startOfMonth, startOfWeek, sugarFor, sum, topByCans, trackedWeeks, wholeNumber, } from "./lib/metrics"; import { exportPayload, parseImport } from "./lib/storage"; import type { DateFilter, EntryDraft, Filters, Flavour, ImportPreview, RedBullEntry } from "./types"; type AppView = "overview" | "logbook" | "trends" | "data"; type AuthMode = "login" | "signup"; type AccentTheme = "blue" | "pink"; type AuthUser = Models.User; type SetupStatus = { state: "checking" | "ok" | "error"; message: string }; const ACCENT_STORAGE_KEY = "red-bull-intake-tracker.accent.v1"; const DEFAULT_FILTERS: Filters = { flavour: "all", dateRange: "all", store: "", from: "", to: "", }; const QUICK_ADDS = [ { label: "Original", flavour: "Original", sizeMl: 250, pricePerCan: 1.75 }, { label: "Sugar Free", flavour: "Sugar Free", sizeMl: 250, pricePerCan: 1.75 }, { label: "Tropical", flavour: "Tropical", sizeMl: 250, pricePerCan: 1.75 }, { label: "473ml Original", flavour: "Original", sizeMl: 473, pricePerCan: 2.85 }, ]; const NAV_ITEMS: Array<{ id: AppView; label: string; icon: LucideIcon }> = [ { id: "overview", label: "Overview", icon: Home }, { id: "logbook", label: "Logbook", icon: CalendarDays }, { id: "trends", label: "Trends", icon: LineChart }, { id: "data", label: "Data", icon: Settings2 }, ]; const ACCENT_OPTIONS: Array<{ id: AccentTheme; label: string }> = [ { id: "blue", label: "Baby blue" }, { id: "pink", label: "Pastel pink" }, ]; function App() { const [themeAccent, setThemeAccent] = useState(() => readStoredAccent()); const [user, setUser] = useState(null); const [authLoading, setAuthLoading] = useState(true); const [authError, setAuthError] = useState(""); const [setupStatus, setSetupStatus] = useState({ state: "checking", message: "Pinging Appwrite...", }); const [entries, setEntries] = useState([]); const [filters, setFilters] = useState(DEFAULT_FILTERS); const [activeView, setActiveView] = useState("overview"); const [isEntryModalOpen, setIsEntryModalOpen] = useState(false); const [editingEntry, setEditingEntry] = useState(null); const [isResetOpen, setIsResetOpen] = useState(false); const [notice, setNotice] = useState("Appwrite session pending."); const [dataLoading, setDataLoading] = useState(false); const [actionLoading, setActionLoading] = useState(null); const [dataError, setDataError] = useState(""); const [importPreview, setImportPreview] = useState(null); const excelFileInputRef = useRef(null); const jsonFileInputRef = useRef(null); useEffect(() => { localStorage.setItem(ACCENT_STORAGE_KEY, themeAccent); }, [themeAccent]); const refreshEntries = useCallback(async (userId: string, showLoader = true) => { if (showLoader) setDataLoading(true); setDataError(""); try { const remoteEntries = await listEntries(userId); setEntries(sortEntries(remoteEntries)); setNotice(`Synced ${remoteEntries.length} Appwrite entr${remoteEntries.length === 1 ? "y" : "ies"}.`); } catch (error) { const message = appwriteErrorMessage(error); setDataError(message); setNotice("Appwrite sync failed."); } finally { if (showLoader) setDataLoading(false); } }, []); useEffect(() => { let mounted = true; async function bootstrap() { setAuthLoading(true); setAuthError(""); try { await pingAppwrite(); if (!mounted) return; setSetupStatus({ state: "ok", message: "Appwrite ping succeeded." }); } catch (error) { if (!mounted) return; setSetupStatus({ state: "error", message: error instanceof Error ? error.message : "Appwrite ping failed.", }); } try { const currentUser = await account.get(); if (!mounted) return; setUser(currentUser); setNotice(`Signed in as ${currentUser.email || currentUser.name || "Appwrite user"}.`); } catch { if (!mounted) return; setUser(null); setNotice("Sign in to sync entries across devices."); } finally { if (mounted) setAuthLoading(false); } } void bootstrap(); return () => { mounted = false; }; }, []); useEffect(() => { if (!user) { setEntries([]); return; } void refreshEntries(user.$id); }, [refreshEntries, user]); useEffect(() => { if (!user) return undefined; const unsubscribe = client.subscribe>( Channel.tablesdb(appwriteConfig.databaseId).table(appwriteConfig.collectionId).row(), (event) => { if (event.payload?.userId === user.$id) { void refreshEntries(user.$id, false); } }, ); return () => unsubscribe(); }, [refreshEntries, user]); const allFlavours = useMemo( () => mergedFlavours(entries.map((entry) => entry.flavour)), [entries], ); const filteredEntries = useMemo( () => sortEntries(applyFilters(entries, filters)), [entries, filters], ); const dashboard = useMemo(() => buildDashboard(entries), [entries]); const chartData = useMemo(() => groupByDay(filteredEntries), [filteredEntries]); const weekData = useMemo(() => groupByWeek(filteredEntries), [filteredEntries]); const flavourData = useMemo(() => groupByFlavour(filteredEntries), [filteredEntries]); const insights = useMemo(() => buildInsights(entries), [entries]); const recentEntries = useMemo(() => entries.slice(0, 5), [entries]); async function login(email: string, password: string) { setActionLoading("auth"); setAuthError(""); try { await account.createEmailPasswordSession({ email, password }); const currentUser = await account.get(); setUser(currentUser); setNotice(`Signed in as ${currentUser.email}.`); } catch (error) { setAuthError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } async function signup(name: string, email: string, password: string) { setActionLoading("auth"); setAuthError(""); try { await account.create({ userId: makeId(), email, password, name: name.trim() || undefined, }); await account.createEmailPasswordSession({ email, password }); const currentUser = await account.get(); setUser(currentUser); setNotice(`Welcome, ${currentUser.name || currentUser.email}.`); } catch (error) { setAuthError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } function startOAuth(provider: "github" | "google") { const selectedProvider = provider === "github" ? OAuthProvider.Github : OAuthProvider.Google; setActionLoading("oauth"); account.createOAuth2Session({ provider: selectedProvider, success: appwriteConfig.oauthSuccessUrl, failure: appwriteConfig.oauthFailureUrl, }); } async function logout() { setActionLoading("logout"); setDataError(""); try { await account.deleteSession({ sessionId: "current" }); setUser(null); setEntries([]); setNotice("Logged out."); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } function openNewEntry() { setEditingEntry(null); setIsEntryModalOpen(true); } async function saveEntry(draft: EntryDraft) { if (!user) return; setActionLoading("save-entry"); setDataError(""); try { const saved = editingEntry ? await updateEntry(user.$id, editingEntry.id, { ...draft, source: editingEntry.source }) : await createEntry(user.$id, { ...draft, source: "manual" }); setEntries((current) => sortEntries(editingEntry ? current.map((entry) => (entry.id === saved.id ? saved : entry)) : [saved, ...current]), ); setNotice(editingEntry ? "Entry updated in Appwrite." : "Entry saved to Appwrite."); setEditingEntry(null); setIsEntryModalOpen(false); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } async function quickAdd(item: (typeof QUICK_ADDS)[number]) { if (!user) return; const meta = flavourMeta(item.flavour); const draft: EntryDraft = { cans: 1, flavour: item.flavour, flavourAccent: meta.accent, sizeMl: item.sizeMl, pricePerCan: item.pricePerCan, dateTime: new Date().toISOString(), sugarFree: Boolean(meta.sugarFree), notes: "Quick add", store: "", source: "quick-add", }; setActionLoading(`quick-${item.label}`); setDataError(""); try { const saved = await createEntry(user.$id, draft); setEntries((current) => sortEntries([saved, ...current])); setNotice(`${item.label} saved to Appwrite.`); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } async function deleteEntry(id: string) { setActionLoading(`delete-${id}`); setDataError(""); try { await deleteEntryDocument(id); setEntries((current) => current.filter((entry) => entry.id !== id)); setNotice("Entry deleted from Appwrite."); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } async function resetAll() { setActionLoading("reset"); setDataError(""); try { await Promise.all(entries.map((entry) => deleteEntryDocument(entry.id))); setEntries([]); setFilters(DEFAULT_FILTERS); setIsResetOpen(false); setNotice("All Appwrite entries deleted."); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } async function exportExcel() { setActionLoading("excel-export"); setDataError(""); try { const blob = await createExcelExport(entries); downloadBlob(blob, `red-bull-intake-${new Date().toISOString().slice(0, 10)}.xlsx`); setNotice("Excel workbook exported."); } catch (error) { setDataError(error instanceof Error ? error.message : "Excel export failed."); } finally { setActionLoading(null); } } async function importExcel(file: File | undefined) { if (!file) return; setActionLoading("excel-import"); setDataError(""); try { const preview = await parseExcelImport(file, entries); setImportPreview(preview); setNotice(`${preview.rows.length} Excel row${preview.rows.length === 1 ? "" : "s"} parsed for review.`); } catch (error) { setDataError(error instanceof Error ? error.message : "Excel import failed."); } finally { if (excelFileInputRef.current) excelFileInputRef.current.value = ""; setActionLoading(null); } } async function confirmExcelImport() { if (!user || !importPreview) return; const drafts = importPreview.rows .filter((row) => row.entry && !row.errors.length && !row.duplicate) .map((row) => row.entry as EntryDraft); if (!drafts.length) { setNotice("No valid new Excel rows to import."); return; } setActionLoading("confirm-excel-import"); setDataError(""); try { const saved = await createEntries(user.$id, drafts); setEntries((current) => sortEntries([...saved, ...current])); setImportPreview(null); setNotice(`${saved.length} Excel row${saved.length === 1 ? "" : "s"} saved to Appwrite.`); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } function exportJson() { const blob = new Blob([exportPayload(entries)], { type: "application/json" }); downloadBlob(blob, `red-bull-intake-${new Date().toISOString().slice(0, 10)}.json`); setNotice("JSON backup exported."); } async function importJson(file: File | undefined) { if (!file || !user) return; setActionLoading("json-import"); setDataError(""); try { const drafts = parseImport(await file.text()); const uniqueDrafts = drafts.filter((draft) => !isDuplicateDraft(entries, draft)); if (!uniqueDrafts.length) { setNotice("No new JSON entries found."); return; } const saved = await createEntries(user.$id, uniqueDrafts.map((draft) => ({ ...draft, source: "json" }))); setEntries((current) => sortEntries([...saved, ...current])); setNotice(`${saved.length} JSON entr${saved.length === 1 ? "y" : "ies"} saved to Appwrite.`); } catch (error) { setDataError(error instanceof Error ? error.message : "JSON import failed."); } finally { if (jsonFileInputRef.current) jsonFileInputRef.current.value = ""; setActionLoading(null); } } if (authLoading) { return ; } if (!user) { return ( ); } return (
void importExcel(event.currentTarget.files?.[0])} /> void importJson(event.currentTarget.files?.[0])} />
void logout()} />
void exportExcel()} onImportExcel={() => excelFileInputRef.current?.click()} onRefresh={() => void refreshEntries(user.$id)} /> {activeView === "overview" && ( void quickAdd(item)} onAdd={openNewEntry} onOpenLogbook={() => setActiveView("logbook")} /> )} {activeView === "logbook" && ( { setEditingEntry(entry); setIsEntryModalOpen(true); }} onDelete={(id) => void deleteEntry(id)} /> )} {activeView === "trends" && ( )} {activeView === "data" && ( void exportExcel()} onImportExcel={() => excelFileInputRef.current?.click()} onExportJson={exportJson} onImportJson={() => jsonFileInputRef.current?.click()} onReset={() => setIsResetOpen(true)} /> )}
{ setIsEntryModalOpen(false); setEditingEntry(null); }} onSave={(draft) => void saveEntry(draft)} /> setImportPreview(null)} onConfirm={() => void confirmExcelImport()} /> setIsResetOpen(false)} onConfirm={() => void resetAll()} />
); } function ShellBackdrop() { return ( <>
); } function LoadingScreen({ setupStatus, themeAccent }: { setupStatus: SetupStatus; themeAccent: AccentTheme }) { return (

Red Bull command centre

{setupStatus.message}

); } function AuthView({ accent, authError, busy, setupStatus, onAccentChange, onLogin, onOAuth, onSignup, }: { accent: AccentTheme; authError: string; busy: boolean; setupStatus: SetupStatus; onAccentChange: (accent: AccentTheme) => void; onLogin: (email: string, password: string) => Promise; onOAuth: (provider: "github" | "google") => void; onSignup: (name: string, email: string, password: string) => Promise; }) { const [mode, setMode] = useState("login"); const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); function submit(event: FormEvent) { event.preventDefault(); if (mode === "signup") { void onSignup(name, email, password); return; } void onLogin(email, password); } return (

Red Bull Tracker App

Glossy intake telemetry with Appwrite authentication, device sync, and finance-grade Excel exports.

{setupStatus.state !== "ok" && (
{setupStatus.message}
)}
{mode === "signup" && ( )} {authError && (
{authError}
)}
OAuth
); } function AuthSignal({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) { return (
); } function AccentPicker({ accent, onChange, }: { accent: AccentTheme; onChange: (accent: AccentTheme) => void; }) { return (
{ACCENT_OPTIONS.map((option) => ( ))}
); } function Sidebar({ accent, activeView, dataLoading, notice, setupStatus, user, onAccentChange, onChange, onLogout, }: { accent: AccentTheme; activeView: AppView; dataLoading: boolean; notice: string; setupStatus: SetupStatus; user: AuthUser; onAccentChange: (accent: AccentTheme) => void; onChange: (view: AppView) => void; onLogout: () => void; }) { return ( ); } function MobileNav({ activeView, onChange }: { activeView: AppView; onChange: (view: AppView) => void }) { return ( ); } function TopBar({ accent, activeView, actionLoading, dataLoading, entries, user, onAccentChange, onAdd, onExportExcel, onImportExcel, onRefresh, }: { accent: AccentTheme; activeView: AppView; actionLoading: string | null; dataLoading: boolean; entries: RedBullEntry[]; user: AuthUser; onAccentChange: (accent: AccentTheme) => void; onAdd: () => void; onExportExcel: () => void; onImportExcel: () => void; onRefresh: () => void; }) { const title = NAV_ITEMS.find((item) => item.id === activeView)?.label ?? "Overview"; const subtitle = new Intl.DateTimeFormat("en-GB", { weekday: "long", day: "numeric", month: "long", }).format(new Date()); return (

{subtitle} {user.email || "Synced user"}

{title}

); } function StatusRail({ actionLoading, dataError, setupStatus, }: { actionLoading: string | null; dataError: string; setupStatus: SetupStatus; }) { if (!actionLoading && !dataError && setupStatus.state === "ok") return null; return (
{actionLoading && (
)} {dataError && (
)} {setupStatus.state === "error" && (
)}
); } function OverviewView({ dashboard, entries, insights, quickAdds, recentEntries, chartData, flavourData, onQuickAdd, onAdd, onOpenLogbook, }: { dashboard: Dashboard; entries: RedBullEntry[]; insights: Insight[]; quickAdds: typeof QUICK_ADDS; recentEntries: RedBullEntry[]; chartData: Array<{ label: string; spend: number; cans: number; caffeine: number; sugar: number }>; flavourData: Array<{ name: string; value: number; spend: number; accent: string }>; onQuickAdd: (item: (typeof QUICK_ADDS)[number]) => void; onAdd: () => void; onOpenLogbook: () => void; }) { return (
{chartData.length ? ( } /> ) : ( )} {recentEntries.length ? (
{recentEntries.map((entry) => ( ))}
) : ( )}
{insights.map((insight) => ( ))}
{flavourData.length ? ( {flavourData.map((entry) => ( ))} } /> ) : ( )}
); } function TodayPanel({ dashboard, entries, onAdd, }: { dashboard: Dashboard; entries: RedBullEntry[]; onAdd: () => void; }) { return (

Today

{dashboard.todayCans}

cans logged

{entries.length ? `${dashboard.allTimeCans} all-time cans` : "Ready for your first entry"}
); } function QuickAddPanel({ items, onQuickAdd }: { items: typeof QUICK_ADDS; onQuickAdd: (item: (typeof QUICK_ADDS)[number]) => void }) { return (
{items.map((item) => { const meta = flavourMeta(item.flavour); return ( ); })}
); } function LogbookView({ entries, totalEntries, filters, flavours, onFilterChange, onAdd, onEdit, onDelete, }: { entries: RedBullEntry[]; totalEntries: number; filters: Filters; flavours: Flavour[]; onFilterChange: (filters: Filters) => void; onAdd: () => void; onEdit: (entry: RedBullEntry) => void; onDelete: (id: string) => void; }) { return (
); } function TrendsView({ chartData, weekData, flavourData, insights, entries, filters, flavours, onFilterChange, }: { chartData: Array<{ label: string; spend: number; cans: number; caffeine: number; sugar: number }>; weekData: Array<{ label: string; spend: number; cans: number }>; flavourData: Array<{ name: string; value: number; spend: number; accent: string }>; insights: Insight[]; entries: RedBullEntry[]; filters: Filters; flavours: Flavour[]; onFilterChange: (filters: Filters) => void; }) { return (
{chartData.length ? ( } /> ) : ( )}
{chartData.length ? ( } /> ) : ( )} {weekData.length ? ( } /> ) : ( )}
{flavourData.length ? ( {flavourData.map((entry) => ( ))} } /> ) : ( )}
{insights.map((insight) => ( ))}
); } function DataView({ dashboard, entries, actionLoading, onExportExcel, onImportExcel, onExportJson, onImportJson, onReset, }: { dashboard: Dashboard; entries: RedBullEntry[]; actionLoading: string | null; onExportExcel: () => void; onImportExcel: () => void; onExportJson: () => void; onImportJson: () => void; onReset: () => void; }) { return (

Configured Appwrite IDs

); } function DataPair({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function MetricTile({ icon: Icon, label, value, detail, accent, }: { icon: LucideIcon; label: string; value: string; detail: string; accent: string; }) { return (

{label}

{value}

{detail}

); } function MiniMetric({ label, value, accent }: { label: string; value: string; accent: string }) { return (

{label}

{value}

); } function InsightCard({ insight }: { insight: Insight }) { return (

{insight.value}

{insight.detail}

); } function AppCard({ title, subtitle, children, }: { title: string; subtitle?: string; children: ReactNode; }) { return (

{title}

{subtitle &&

{subtitle}

}
{children}
); } function ChartTooltip({ active, payload, label, }: { active?: boolean; payload?: Array<{ name: string; value: number; color?: string }>; label?: string; }) { if (!active || !payload?.length) return null; return (

{label}

{payload.map((item) => (

{item.name}: {formatMetricValue(item.name, item.value)}

))}
); } function EmptyState({ title, copy, actionLabel, onAction, }: { title: string; copy: string; actionLabel?: string; onAction?: () => void; }) { return (

{title}

{copy}

{actionLabel && onAction && ( )}
); } function FiltersPanel({ filters, flavours, compact = false, onChange, }: { filters: Filters; flavours: Flavour[]; compact?: boolean; onChange: (filters: Filters) => void; }) { const set = (key: Key, value: Filters[Key]) => { onChange({ ...filters, [key]: value }); }; return (
{filters.dateRange === "custom" && ( <> )}
); } function EntryLedger({ entries, totalEntries, onAdd, onEdit, onDelete, }: { entries: RedBullEntry[]; totalEntries: number; onAdd: () => void; onEdit: (entry: RedBullEntry) => void; onDelete: (id: string) => void; }) { return ( {entries.length ? (
{entries.map((entry) => ( ))}
) : ( )}
); } function EntryRow({ entry, onEdit, onDelete, }: { entry: RedBullEntry; onEdit: (entry: RedBullEntry) => void; onDelete: (id: string) => void; }) { return (

{entry.flavour}

{entry.cans} can{entry.cans === 1 ? "" : "s"} · {entry.sizeMl}ml {entry.source}

{humanDateTime(entry.dateTime)}

{currency.format(spendFor(entry))} · {wholeNumber.format(caffeineFor(entry))}mg caffeine · {oneDecimal.format(sugarFor(entry))}g sugar

{(entry.store || entry.notes) && (

{entry.store ? `${entry.store}` : ""} {entry.store && entry.notes ? " · " : ""} {entry.notes}

)}
); } function MiniEntry({ entry }: { entry: RedBullEntry }) { return (

{entry.flavour}

{humanDateTime(entry.dateTime)}

{currency.format(spendFor(entry))}

); } function DisclaimerCard() { return (

Estimates

Caffeine and sugar values are estimates. Check the can label for exact nutritional information.

); } function EntryModal({ open, entry, flavours, saving, onClose, onSave, }: { open: boolean; entry: RedBullEntry | null; flavours: Flavour[]; saving: boolean; onClose: () => void; onSave: (draft: EntryDraft) => void; }) { const firstFieldRef = useRef(null); const initialFlavour = entry?.flavour ?? DEFAULT_FLAVOUR.name; const [selectedFlavour, setSelectedFlavour] = useState(initialFlavour); const [customFlavour, setCustomFlavour] = useState(""); const [customAccent, setCustomAccent] = useState("#39d5ff"); const [cans, setCans] = useState(entry?.cans.toString() ?? "1"); const [sizePreset, setSizePreset] = useState(sizeToPreset(entry?.sizeMl ?? 250)); const [customSize, setCustomSize] = useState(entry?.sizeMl.toString() ?? "250"); const [pricePerCan, setPricePerCan] = useState(entry?.pricePerCan.toString() ?? "1.75"); const [dateTime, setDateTime] = useState(formatLocalInput(entry ? new Date(entry.dateTime) : new Date())); const [store, setStore] = useState(entry?.store ?? ""); const [notes, setNotes] = useState(entry?.notes ?? ""); const [sugarFree, setSugarFree] = useState(entry?.sugarFree ?? false); const [caffeineOverride, setCaffeineOverride] = useState(entry?.caffeineMgPerCan?.toString() ?? ""); useEffect(() => { if (!open) return; const editingCustom = entry && !BUILT_IN_FLAVOURS.some((flavour) => flavour.name === entry.flavour); setSelectedFlavour(editingCustom ? entry.flavour : entry?.flavour ?? DEFAULT_FLAVOUR.name); setCustomFlavour(editingCustom ? entry.flavour : ""); setCustomAccent(entry?.flavourAccent ?? "#39d5ff"); setCans(entry?.cans.toString() ?? "1"); setSizePreset(sizeToPreset(entry?.sizeMl ?? 250)); setCustomSize(entry?.sizeMl.toString() ?? "250"); setPricePerCan(entry?.pricePerCan.toString() ?? defaultPriceForSize(250).toString()); setDateTime(formatLocalInput(entry ? new Date(entry.dateTime) : new Date())); setStore(entry?.store ?? ""); setNotes(entry?.notes ?? ""); setSugarFree(entry?.sugarFree ?? false); setCaffeineOverride(entry?.caffeineMgPerCan?.toString() ?? ""); window.setTimeout(() => firstFieldRef.current?.focus(), 80); }, [entry, open]); useEffect(() => { if (!open) return; const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") onClose(); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [onClose, open]); const selectedMeta = flavourMeta(selectedFlavour); const isOther = selectedFlavour === "Other"; const numericSize = Math.max(1, sizePreset === "custom" ? Number(customSize) || 250 : Number(sizePreset)); const finalAccent = isOther ? customAccent : selectedMeta.accent; const caffeinePreview = caffeinePerCan( numericSize, sizePreset === "custom" && caffeineOverride.trim() ? Number(caffeineOverride) : undefined, ); function submit(event: FormEvent) { event.preventDefault(); const numericCans = Math.max(0.25, Number(cans) || 1); const numericPrice = Math.max(0, Number(pricePerCan) || 0); const finalFlavour = isOther ? customFlavour.trim() || "Other" : selectedFlavour; const meta = flavourMeta(finalFlavour); const override = sizePreset === "custom" && caffeineOverride.trim() ? Math.max(0, Number(caffeineOverride) || 0) : undefined; onSave({ cans: numericCans, flavour: finalFlavour, flavourAccent: isOther ? customAccent || accentForCustomFlavour(finalFlavour) : meta.accent, sizeMl: numericSize, pricePerCan: numericPrice, dateTime: new Date(dateTime).toISOString(), notes: notes.trim(), store: store.trim(), sugarFree: sugarFree || Boolean(meta.sugarFree), caffeineMgPerCan: override, source: entry?.source ?? "manual", }); } return ( {open && (

Intake details

{entry ? "Edit entry" : "Add intake"}

{isOther && ( <> )} {sizePreset === "custom" && ( <> )}