import type { Models } from "appwrite"; import { Activity, AlertTriangle, CalendarDays, Camera, ChevronRight, Cloud, Command, Edit3, FileJson, FileSpreadsheet, Gauge, Home, LineChart, Loader2, LogIn, LogOut, Plus, PoundSterling, RefreshCcw, RotateCcw, Search, Settings2, Sparkles, 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 { APP_THEMES, THEME_STORAGE_KEY, getThemeById, normaliseThemeId, readStoredThemeId, type AppTheme, } from "./data/themes"; import { themeTokensToStyle } from "./lib/themeTokens"; import { account, appwriteConfig, Channel, client, pingAppwrite } from "./lib/appwrite"; import { appwriteErrorMessage, createEntries, createEntry, deleteEntry as deleteEntryDocument, isDuplicateDraft, listEntries, updateEntry, } from "./lib/appwriteEntries"; import { BarcodeScannerModal } from "./components/BarcodeScannerModal"; import { DailyLimitsCard } from "./components/DailyLimitsCard"; import { LegalFootnote } from "./components/LegalFootnote"; import { LimitsSettingsForm } from "./components/LimitsSettingsForm"; import { OnboardingScreen } from "./components/OnboardingScreen"; import { buildDynamicGreeting } from "./lib/greeting"; import { evaluateLimits, limitStatusMessage, mergePrefsWithLimits, parseUserLimits, } from "./lib/userLimits"; import { createExcelExport, downloadBlob, parseExcelImport } from "./lib/excel"; import { caffeineFor, caffeinePerCan, canLimitFromSpend, 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, LimitCheckResult, RedBullEntry, UserLimits, } from "./types"; type AppView = "overview" | "logbook" | "trends" | "settings"; type AuthMode = "login" | "signup"; type AuthUser = Models.User; type SetupStatus = { state: "checking" | "ok" | "error"; message: string }; type ForecastPoint = { label: string; current: number; lower: number; limit?: number; }; type PendingLimitAction = { kind: "save" | "quick"; draft: EntryDraft; editingId?: string; quickLabel?: string; }; const MATERIAL_ACCENTS = { primary: "var(--chart-primary)", secondary: "var(--chart-secondary)", tertiary: "var(--chart-tertiary)", error: "var(--chart-error)", custom: "#b85d84", }; 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: "Iced Vanilla", flavour: "Iced Vanilla", 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: "settings", label: "Settings", icon: Settings2 }, ]; function App() { const [themeId, setThemeId] = useState(() => readStoredThemeId()); const activeTheme = useMemo(() => getThemeById(themeId), [themeId]); const shellStyle = useMemo(() => themeTokensToStyle(activeTheme.tokens), [activeTheme]); 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 [entryInitialDraft, setEntryInitialDraft] = useState(null); const [editingEntry, setEditingEntry] = useState(null); const [isBarcodeScannerOpen, setIsBarcodeScannerOpen] = useState(false); const [isResetOpen, setIsResetOpen] = useState(false); const [notice, setNotice] = useState("Appwrite session pending."); const [dataLoading, setDataLoading] = useState(false); const [busyAction, setBusyAction] = useState(null); const [syncError, setSyncError] = useState(""); const [importPreview, setImportPreview] = useState(null); const [userLimits, setUserLimits] = useState({}); const [limitConfirmOpen, setLimitConfirmOpen] = useState(false); const [limitConfirmMessage, setLimitConfirmMessage] = useState(""); const [pendingLimitAction, setPendingLimitAction] = useState(null); const [setupOpen, setSetupOpen] = useState(false); const excelFileInputRef = useRef(null); const jsonFileInputRef = useRef(null); useEffect(() => { localStorage.setItem(THEME_STORAGE_KEY, normaliseThemeId(themeId)); }, [themeId]); const refreshEntries = useCallback(async (userId: string, showLoader = true) => { if (showLoader) setDataLoading(true); setSyncError(""); 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); setSyncError(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); setUserLimits(parseUserLimits(currentUser.prefs)); if (typeof currentUser.prefs.themeId === "string" && currentUser.prefs.themeId) { setThemeId(normaliseThemeId(currentUser.prefs.themeId)); } setNotice(`Signed in as ${currentUser.email || currentUser.name || "Appwrite user"}.`); if (!currentUser.prefs.onboarded) { setSetupOpen(true); } } 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 entriesInView = useMemo( () => sortEntries(applyFilters(entries, filters)), [entries, filters], ); const summary = useMemo(() => buildDashboard(entries), [entries]); const limitCheck = useMemo(() => evaluateLimits(userLimits, entries), [userLimits, entries]); const chartData = useMemo(() => groupByDay(entriesInView), [entriesInView]); const weekData = useMemo(() => groupByWeek(entriesInView), [entriesInView]); const flavourData = useMemo(() => groupByFlavour(entriesInView), [entriesInView]); const insights = useMemo(() => buildInsights(entries), [entries]); const recentEntries = useMemo(() => entries.slice(0, 5), [entries]); async function login(email: string, password: string) { setBusyAction("auth"); setAuthError(""); try { await account.createEmailPasswordSession({ email, password }); const currentUser = await account.get(); setUser(currentUser); setUserLimits(parseUserLimits(currentUser.prefs)); if (typeof currentUser.prefs.themeId === "string" && currentUser.prefs.themeId) { setThemeId(normaliseThemeId(currentUser.prefs.themeId)); } setNotice(`Signed in as ${currentUser.email}.`); if (!currentUser.prefs.onboarded) { setSetupOpen(true); } } catch (error) { setAuthError(appwriteErrorMessage(error)); } finally { setBusyAction(null); } } async function signup(name: string, email: string, password: string) { setBusyAction("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}.`); setSetupOpen(true); } catch (error) { setAuthError(appwriteErrorMessage(error)); } finally { setBusyAction(null); } } async function logout() { setBusyAction("logout"); setSyncError(""); try { await account.deleteSession({ sessionId: "current" }); setUser(null); setEntries([]); setNotice("Logged out."); } catch (error) { setSyncError(appwriteErrorMessage(error)); } finally { setBusyAction(null); } } function openNewEntry() { setEditingEntry(null); setEntryInitialDraft(null); setIsEntryModalOpen(true); } function openBarcodeScanner() { setIsBarcodeScannerOpen(true); } function addBarcodeDraft(draft: EntryDraft) { setIsBarcodeScannerOpen(false); saveDraftWithLimitCheck(draft); } function editBarcodeDraft(draft: EntryDraft) { setIsBarcodeScannerOpen(false); setEditingEntry(null); setEntryInitialDraft(draft); setIsEntryModalOpen(true); } async function saveUserLimits(next: UserLimits) { if (!user) return; setBusyAction("save-limits"); setSyncError(""); try { const prefs = mergePrefsWithLimits(user.prefs, next); await account.updatePrefs(prefs); const currentUser = await account.get(); setUser(currentUser); setUserLimits(parseUserLimits(currentUser.prefs)); setNotice("Daily limits saved to your account."); } catch (error) { setSyncError(appwriteErrorMessage(error)); } finally { setBusyAction(null); } } async function saveOnboarding(limits: UserLimits, onboardingThemeId: string) { if (!user) return; setBusyAction("save-onboarding"); setSyncError(""); try { const limitsPrefs = mergePrefsWithLimits(user.prefs, limits); const nextPrefs = { ...limitsPrefs, themeId: onboardingThemeId, onboarded: true, }; await account.updatePrefs(nextPrefs); const currentUser = await account.get(); setUser(currentUser); setUserLimits(parseUserLimits(currentUser.prefs)); setThemeId(onboardingThemeId); setSetupOpen(false); setNotice("Setup saved."); } catch (error) { setSyncError(appwriteErrorMessage(error)); } finally { setBusyAction(null); } } async function saveDraft(action: PendingLimitAction) { if (!user) return; const loadingKey = action.kind === "quick" ? `quick-${action.quickLabel ?? "add"}` : "save-entry"; setBusyAction(loadingKey); setSyncError(""); try { const editing = action.editingId ? entries.find((entry) => entry.id === action.editingId) : null; const saved = editing ? await updateEntry(user.$id, editing.id, { ...action.draft, source: editing.source }) : await createEntry(user.$id, { ...action.draft, source: action.draft.source ?? "manual" }); setEntries((current) => sortEntries(editing ? current.map((entry) => (entry.id === saved.id ? saved : entry)) : [saved, ...current]), ); setNotice(editing ? "Entry updated in Appwrite." : "Entry saved to Appwrite."); setEditingEntry(null); setEntryInitialDraft(null); setIsEntryModalOpen(false); } catch (error) { setSyncError(appwriteErrorMessage(error)); } finally { setBusyAction(null); setLimitConfirmOpen(false); setPendingLimitAction(null); setLimitConfirmMessage(""); } } function saveDraftWithLimitCheck(draft: EntryDraft, editingId?: string) { const check = evaluateLimits(userLimits, entries, { draft, excludeEntryId: editingId }); if (check.violations.length) { setPendingLimitAction({ kind: "save", draft, editingId }); setLimitConfirmMessage(limitStatusMessage(check.violations, check, userLimits)); setLimitConfirmOpen(true); return; } void saveDraft({ kind: "save", draft, editingId }); } async function saveEntryDraft(draft: EntryDraft) { if (!user) return; saveDraftWithLimitCheck(draft, editingEntry?.id); } 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", }; const check = evaluateLimits(userLimits, entries, { draft }); if (check.violations.length) { setPendingLimitAction({ kind: "quick", draft, quickLabel: item.label }); setLimitConfirmMessage(limitStatusMessage(check.violations, check, userLimits)); setLimitConfirmOpen(true); return; } void saveDraft({ kind: "quick", draft, quickLabel: item.label }); } function confirmLimitOverride() { if (!pendingLimitAction) return; void saveDraft(pendingLimitAction); } async function deleteEntry(id: string) { setBusyAction(`delete-${id}`); setSyncError(""); try { await deleteEntryDocument(id); setEntries((current) => current.filter((entry) => entry.id !== id)); setNotice("Entry deleted from Appwrite."); } catch (error) { setSyncError(appwriteErrorMessage(error)); } finally { setBusyAction(null); } } async function resetAll() { setBusyAction("reset"); setSyncError(""); try { await Promise.all(entries.map((entry) => deleteEntryDocument(entry.id))); setEntries([]); setFilters(DEFAULT_FILTERS); setIsResetOpen(false); setNotice("All Appwrite entries deleted."); } catch (error) { setSyncError(appwriteErrorMessage(error)); } finally { setBusyAction(null); } } async function exportExcel() { setBusyAction("excel-export"); setSyncError(""); try { const blob = await createExcelExport(entries); downloadBlob(blob, `red-bull-intake-${new Date().toISOString().slice(0, 10)}.xlsx`); setNotice("Excel workbook exported."); } catch (error) { setSyncError(error instanceof Error ? error.message : "Excel export failed."); } finally { setBusyAction(null); } } async function importExcel(file: File | undefined) { if (!file) return; setBusyAction("excel-import"); setSyncError(""); 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) { setSyncError(error instanceof Error ? error.message : "Excel import failed."); } finally { if (excelFileInputRef.current) excelFileInputRef.current.value = ""; setBusyAction(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; } setBusyAction("confirm-excel-import"); setSyncError(""); 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) { setSyncError(appwriteErrorMessage(error)); } finally { setBusyAction(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; setBusyAction("json-import"); setSyncError(""); 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) { setSyncError(error instanceof Error ? error.message : "JSON import failed."); } finally { if (jsonFileInputRef.current) jsonFileInputRef.current.value = ""; setBusyAction(null); } } if (authLoading) { return ; } if (!user) { return ( ); } return (
{setupOpen && user && ( setSetupOpen(false)} initialLimits={userLimits} /> )} void importExcel(event.currentTarget.files?.[0])} /> void importJson(event.currentTarget.files?.[0])} />
setActiveView("settings")} />
{activeView === "overview" && ( void quickAdd(item)} onAdd={openNewEntry} onScan={openBarcodeScanner} onOpenLogbook={() => setActiveView("logbook")} onOpenSettings={() => setActiveView("settings")} /> )} {activeView === "logbook" && ( { setEditingEntry(entry); setIsEntryModalOpen(true); }} onDelete={(id) => void deleteEntry(id)} /> )} {activeView === "trends" && ( void saveUserLimits(next)} /> )} {activeView === "settings" && ( void exportExcel()} onImportExcel={() => excelFileInputRef.current?.click()} onExportJson={exportJson} onImportJson={() => jsonFileInputRef.current?.click()} onLogout={() => void logout()} onReset={() => setIsResetOpen(true)} onThemeChange={setThemeId} onSaveLimits={(next) => void saveUserLimits(next)} onRerunOnboarding={() => setSetupOpen(true)} /> )}
{ setIsEntryModalOpen(false); setEditingEntry(null); setEntryInitialDraft(null); }} onSave={(draft) => void saveEntryDraft(draft)} /> setIsBarcodeScannerOpen(false)} onEditBeforeAdding={editBarcodeDraft} /> setImportPreview(null)} onConfirm={() => void confirmExcelImport()} /> setIsResetOpen(false)} onConfirm={() => void resetAll()} /> { setLimitConfirmOpen(false); setPendingLimitAction(null); setLimitConfirmMessage(""); }} onConfirm={confirmLimitOverride} />
); } function ShellBackdrop() { return ( <>
); } function LoadingScreen({ setupStatus, shellStyle, themeId, }: { setupStatus: SetupStatus; shellStyle: CSSProperties; themeId: string; }) { return (

Red Bull tracker

{setupStatus.message}

); } function AuthView({ authError, busy, setupStatus, shellStyle, themeId, onLogin, onSignup, }: { authError: string; busy: boolean; setupStatus: SetupStatus; shellStyle: CSSProperties; themeId: string; onLogin: (email: string, password: string) => Promise; 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

Track intake, sync across devices.

{setupStatus.state !== "ok" && (
{setupStatus.message}
)}
{mode === "signup" && ( )} {authError && (
{authError}
)}
); } function ThemePicker({ themeId, onChange, }: { themeId: string; onChange: (id: string) => void; }) { const activeTheme = getThemeById(themeId); return (
Button
Panel
Chart
{APP_THEMES.map((theme) => ( ))}

Current theme: {activeTheme.label}

); } function Sidebar({ activeView, dataLoading, notice, setupStatus, user, onAdd, onScan, onChange, onOpenSettings, }: { activeView: AppView; dataLoading: boolean; notice: string; setupStatus: SetupStatus; user: AuthUser; onAdd: () => void; onScan: () => void; onChange: (view: AppView) => void; onOpenSettings: () => void; }) { return ( ); } function MobileNav({ activeView, onChange }: { activeView: AppView; onChange: (view: AppView) => void }) { return ( ); } function TopBar({ activeView, busyAction, onAdd, onScan, className = "", }: { activeView: AppView; busyAction: string | null; onAdd: () => void; onScan: () => void; className?: string; }) { const activeItem = NAV_ITEMS.find((item) => item.id === activeView) ?? NAV_ITEMS[0]; const ActiveIcon = activeItem.icon; const title = activeItem.label; const subtitle = new Intl.DateTimeFormat("en-GB", { weekday: "long", day: "numeric", month: "long", }).format(new Date()); return (

{subtitle}

{title}

); } function StatusRail({ busyAction, syncError, setupStatus, }: { busyAction: string | null; syncError: string; setupStatus: SetupStatus; }) { if (!busyAction && !syncError && setupStatus.state === "ok") return null; return (
{busyAction && (
)} {syncError && (
)} {setupStatus.state === "error" && (
)}
); } function OverviewView({ summary, entries, insights, quickAdds, recentEntries, chartData, flavourData, user, userLimits, limitCheck, onQuickAdd, onAdd, onScan, onOpenLogbook, onOpenSettings, }: { summary: 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 }>; user: AuthUser; userLimits: UserLimits; limitCheck: LimitCheckResult; onQuickAdd: (item: (typeof QUICK_ADDS)[number]) => void; onAdd: () => void; onScan: () => void; onOpenLogbook: () => void; onOpenSettings: () => void; }) { const todaySpendRaw = limitCheck.todaySpend; const spendLimitDetail = userLimits.dailySpendLimit != null ? `${currency.format(todaySpendRaw)} of ${currency.format(userLimits.dailySpendLimit)} today` : `${summary.monthSpend} this month`; return (
{limitCheck.violations.length ? (
) : null}
{chartData.length ? (
} />
) : ( )}
{recentEntries.length ? (
{recentEntries.map((entry) => ( ))}
) : ( )}
{insights.map((insight) => ( ))}
{flavourData.length ? (
{flavourData.map((entry) => ( ))} } />
) : ( )}
); } function GreetingPanel({ summary, user, userLimits, limitCheck, onAdd, onScan, }: { summary: Dashboard; user: AuthUser; userLimits: UserLimits; limitCheck: LimitCheckResult; onAdd: () => void; onScan: () => void; }) { const todayNumber = Number.parseFloat(summary.todayCans) || 0; const canLimit = userLimits.dailyCanLimit; const name = firstName(user); const greeting = buildDynamicGreeting({ name, todayCans: todayNumber, favouriteFlavour: summary.favouriteFlavour, currentStreak: Number.parseInt(summary.currentStreak, 10) || 0, todayCaffeineMg: Number.parseFloat(summary.todayCaffeine.replace(/[^\d.]/g, "")) || 0, allTimeCans: Number.parseFloat(summary.allTimeCans) || 0, dailyCanLimit: canLimit, limitCheck, }); return (
{userInitial(user)}

{greeting.badge}

{name}

{greeting.subline}

); } function statHint(label: string) { return label === "Caffeine" || label === "Sugar" ? "estimated from the logged can. check the label if it matters." : undefined; } function WellnessPill({ label, value }: { label: string; value: string }) { return (
{label} {value}
); } function TodayPanel({ summary, entries, userLimits, limitCheck, onAdd, onScan, }: { summary: Dashboard; entries: RedBullEntry[]; userLimits: UserLimits; limitCheck: LimitCheckResult; onAdd: () => void; onScan: () => void; }) { const limitSummary = userLimits.dailyCanLimit != null || userLimits.dailySpendLimit != null ? limitCheck.violations.length ? limitStatusMessage(limitCheck.violations, limitCheck, userLimits) : `${limitCheck.todayCans} cans · ${currency.format(limitCheck.todaySpend)} spent today` : ""; return (

Today

{summary.todayCans}

cans logged

{limitSummary ?

{limitSummary}

: null}
{entries.length ? `${summary.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, userLimits, onFilterChange, onSaveLimits, }: { 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[]; userLimits: UserLimits; onFilterChange: (filters: Filters) => void; onSaveLimits: (limits: UserLimits) => void; }) { return (
{chartData.length ? ( } /> ) : ( )}
{chartData.length ? ( } /> ) : ( )} {weekData.length ? ( } /> ) : ( )}
{flavourData.length ? ( {flavourData.map((entry) => ( ))} } /> ) : ( )}
{insights.map((insight) => ( ))}
); } function SpendForecastCard({ entries, userLimits, onSaveLimits, }: { entries: RedBullEntry[]; userLimits: UserLimits; onSaveLimits?: (limits: UserLimits) => void; }) { const [projectionDays, setProjectionDays] = useState<7 | 30 | 90 | 365>(30); const now = useMemo(() => new Date(), []); const firstEntryDate = useMemo(() => { if (!entries.length) return now; return new Date( [...entries].sort( (a, b) => new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime(), )[0].dateTime, ); }, [entries, now]); const activePeriodDays = useMemo(() => { const diffTime = Math.abs(now.getTime() - firstEntryDate.getTime()); return Math.max(1, Math.ceil(diffTime / (1000 * 60 * 60 * 24))); }, [firstEntryDate, now]); const stats = useMemo(() => { const periodStart = new Date(now.getTime() - activePeriodDays * 86_400_000); const recentEntries = entriesInRange(entries, periodStart, now); const totalSpend = sum(recentEntries, spendFor); const totalCans = sum(recentEntries, (entry) => entry.cans); const hasData = recentEntries.length > 0; return { hasData, avgDailySpend: hasData ? totalSpend / activePeriodDays : 0, avgDailyCans: hasData ? totalCans / activePeriodDays : 0, }; }, [entries, activePeriodDays, now]); const projectionData = useMemo(() => { return Array.from({ length: projectionDays }).map((_, index) => { const day = index + 1; const dataPoint: ForecastPoint = { label: `day ${day}`, current: Number((day * stats.avgDailySpend).toFixed(2)), lower: Number((day * stats.avgDailySpend * 0.8).toFixed(2)), }; if (userLimits.dailySpendLimit != null) { dataPoint.limit = Number((day * userLimits.dailySpendLimit).toFixed(2)); } return dataPoint; }); }, [projectionDays, stats.avgDailySpend, userLimits?.dailySpendLimit]); if (!stats.hasData) { return ( ); } const projectedSpend = stats.avgDailySpend * projectionDays; const projectedCans = stats.avgDailyCans * projectionDays; const lowerSpend = projectedSpend * 0.8; const possibleSavings = projectedSpend - lowerSpend; const saveLowerLimit = () => { if (!onSaveLimits) return; const lowerDailyLimit = Math.round(stats.avgDailySpend * 0.8 * 100) / 100; const size = userLimits.limitCanSizeMl ?? 250; onSaveLimits({ ...userLimits, limitCanSizeMl: size, dailySpendLimit: lowerDailyLimit, dailyCanLimit: canLimitFromSpend(lowerDailyLimit, size), }); }; return (

Forecast window

{([7, 30, 90, 365] as const).map((days) => ( ))}
Projected spend

{currency.format(projectedSpend)}

~{oneDecimal.format(projectedCans)} cans logged
20 percent lower

{currency.format(lowerSpend)}

~{oneDecimal.format(projectedCans * 0.8)} cans logged
Possible savings

{currency.format(possibleSavings)}

{onSaveLimits && ( )}
`£${val}`} /> } /> {userLimits.dailySpendLimit != null && ( )}
); } function SettingsView({ activeTheme, summary, dataLoading, entries, notice, setupStatus, themeId, user, userLimits, limitCheck, busyAction, onExportExcel, onImportExcel, onExportJson, onImportJson, onLogout, onReset, onThemeChange, onSaveLimits, onRerunOnboarding, }: { activeTheme: AppTheme; summary: Dashboard; dataLoading: boolean; entries: RedBullEntry[]; notice: string; setupStatus: SetupStatus; themeId: string; user: AuthUser | null; userLimits: UserLimits; limitCheck: LimitCheckResult; busyAction: string | null; onExportExcel: () => void; onImportExcel: () => void; onExportJson: () => void; onImportJson: () => void; onLogout: () => void; onReset: () => void; onThemeChange: (id: string) => void; onSaveLimits: (limits: UserLimits) => void; onRerunOnboarding: () => void; }) { return (

Configured Appwrite IDs

{userInitial(user)}

{user?.name || "Appwrite user"}

{user?.email}

{dataLoading ?

{setupStatus.message}

); } 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 EntryModal({ open, entry, initialDraft, flavours, saving, onClose, onSave, }: { open: boolean; entry: RedBullEntry | null; initialDraft: EntryDraft | null; flavours: Flavour[]; saving: boolean; onClose: () => void; onSave: (draft: EntryDraft) => void; }) { const firstFieldRef = useRef(null); const activeDraft = entry ?? initialDraft; const initialFlavour = activeDraft?.flavour ?? DEFAULT_FLAVOUR.name; const [selectedFlavour, setSelectedFlavour] = useState(initialFlavour); const [customFlavour, setCustomFlavour] = useState(""); const [customAccent, setCustomAccent] = useState(MATERIAL_ACCENTS.custom); const [cans, setCans] = useState(activeDraft?.cans.toString() ?? "1"); const [sizePreset, setSizePreset] = useState(sizeToPreset(activeDraft?.sizeMl ?? 250)); const [customSize, setCustomSize] = useState(activeDraft?.sizeMl.toString() ?? "250"); const [pricePerCan, setPricePerCan] = useState(activeDraft?.pricePerCan.toString() ?? "1.75"); const [dateTime, setDateTime] = useState(formatLocalInput(activeDraft ? new Date(activeDraft.dateTime) : new Date())); const [store, setStore] = useState(activeDraft?.store ?? ""); const [notes, setNotes] = useState(activeDraft?.notes ?? ""); const [sugarFree, setSugarFree] = useState(activeDraft?.sugarFree ?? false); const [caffeineOverride, setCaffeineOverride] = useState(activeDraft?.caffeineMgPerCan?.toString() ?? ""); useEffect(() => { if (!open) return; const draft = entry ?? initialDraft; const editingCustom = draft && !BUILT_IN_FLAVOURS.some((flavour) => flavour.name === draft.flavour); setSelectedFlavour(editingCustom ? draft.flavour : draft?.flavour ?? DEFAULT_FLAVOUR.name); setCustomFlavour(editingCustom ? draft.flavour : ""); setCustomAccent(draft?.flavourAccent ?? MATERIAL_ACCENTS.custom); setCans(draft?.cans.toString() ?? "1"); setSizePreset(sizeToPreset(draft?.sizeMl ?? 250)); setCustomSize(draft?.sizeMl.toString() ?? "250"); setPricePerCan(draft?.pricePerCan.toString() ?? defaultPriceForSize(250).toString()); setDateTime(formatLocalInput(draft ? new Date(draft.dateTime) : new Date())); setStore(draft?.store ?? ""); setNotes(draft?.notes ?? ""); setSugarFree(draft?.sugarFree ?? false); setCaffeineOverride(draft?.caffeineMgPerCan?.toString() ?? ""); window.setTimeout(() => firstFieldRef.current?.focus(), 80); }, [entry, initialDraft, 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, ); const draftPreview = useMemo(() => { 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; return { 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 ?? initialDraft?.source ?? "manual", }; }, [ cans, pricePerCan, isOther, customFlavour, selectedFlavour, customAccent, numericSize, dateTime, notes, store, sugarFree, sizePreset, caffeineOverride, entry?.source, initialDraft?.source, ]); function submit(event: FormEvent) { event.preventDefault(); if (!draftPreview) return; onSave(draftPreview); } return ( {open && (

Intake details

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

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