import type { Models } from "appwrite"; import { Activity, AlertTriangle, CalendarDays, ChevronRight, Cloud, Command, Edit3, FileJson, FileSpreadsheet, Gauge, Github, Home, Info, LineChart, Loader2, LogIn, LogOut, MessageCircle, 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_CATEGORIES, THEME_STORAGE_KEY, getThemeById, readStoredThemeId, type AppTheme, type ThemeCategory, } from "./data/themes"; import { themeTokensToStyle } from "./lib/themeTokens"; import { account, appwriteConfig, Channel, client, OAuthProvider, pingAppwrite } from "./lib/appwrite"; import { appwriteErrorMessage, createEntries, createEntry, deleteEntry as deleteEntryDocument, isDuplicateDraft, listEntries, updateEntry, } from "./lib/appwriteEntries"; import { CoachPanel } from "./components/CoachPanel"; import { DailyLimitsCard } from "./components/DailyLimitsCard"; import { LimitsSettingsForm } from "./components/LimitsSettingsForm"; import { OnboardingScreen } from "./components/OnboardingScreen"; import { buildDynamicGreeting } from "./lib/greeting"; import { evaluateLimits, limitStatusMessage, mergePrefsWithLimits, parseUserLimits, } from "./lib/userLimits"; import type { CoachSession } from "./lib/useCoachSession"; import { useCoachSession } from "./lib/useCoachSession"; 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, LimitCheckResult, RedBullEntry, UserLimits, } from "./types"; type AppView = "overview" | "logbook" | "trends" | "coach" | "settings"; type AuthMode = "login" | "signup"; type AuthUser = Models.User; type SetupStatus = { state: "checking" | "ok" | "error"; message: string }; type PendingLimitAction = { kind: "save" | "quick"; draft: EntryDraft; editingId?: string; quickLabel?: string; }; 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: "coach", label: "Coach", icon: MessageCircle }, { id: "settings", label: "Settings", icon: Settings2 }, ]; const MATERIAL_ACCENTS = { primary: "var(--chart-primary)", secondary: "var(--chart-secondary)", tertiary: "var(--chart-tertiary)", error: "var(--chart-error)", custom: "#b85d84", }; 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 [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 [userLimits, setUserLimits] = useState({}); const [limitConfirmOpen, setLimitConfirmOpen] = useState(false); const [limitConfirmMessage, setLimitConfirmMessage] = useState(""); const [pendingLimitAction, setPendingLimitAction] = useState(null); const [showOnboarding, setShowOnboarding] = useState(false); const excelFileInputRef = useRef(null); const jsonFileInputRef = useRef(null); useEffect(() => { localStorage.setItem(THEME_STORAGE_KEY, themeId); }, [themeId]); 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); setUserLimits(parseUserLimits(currentUser.prefs)); if (typeof currentUser.prefs.themeId === "string" && currentUser.prefs.themeId) { setThemeId(currentUser.prefs.themeId); } setNotice(`Signed in as ${currentUser.email || currentUser.name || "Appwrite user"}.`); if (!currentUser.prefs.onboarded) { setShowOnboarding(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 filteredEntries = useMemo( () => sortEntries(applyFilters(entries, filters)), [entries, filters], ); const dashboard = useMemo(() => buildDashboard(entries), [entries]); const limitCheck = useMemo(() => evaluateLimits(userLimits, entries), [userLimits, 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]); const coachSession = useCoachSession( user ?? ({ $id: "", email: "", name: "" } as AuthUser), dashboard, entries, userLimits, limitCheck, ); async function login(email: string, password: string) { setActionLoading("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(currentUser.prefs.themeId); } setNotice(`Signed in as ${currentUser.email}.`); if (!currentUser.prefs.onboarded) { setShowOnboarding(true); } } 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); setUserLimits(parseUserLimits(currentUser.prefs)); setNotice(`Welcome, ${currentUser.name || currentUser.email}.`); setShowOnboarding(true); } 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([]); setUserLimits({}); setNotice("Logged out."); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } function openNewEntry() { setEditingEntry(null); setIsEntryModalOpen(true); } async function saveUserLimits(next: UserLimits) { if (!user) return; setActionLoading("save-limits"); setDataError(""); 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) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } async function saveOnboarding(limits: UserLimits, onboardingThemeId: string) { if (!user) return; setActionLoading("save-onboarding"); setDataError(""); 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); setShowOnboarding(false); setNotice("Onboarding limits and theme saved successfully."); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); } } async function persistEntry(action: PendingLimitAction) { if (!user) return; const loadingKey = action.kind === "quick" ? `quick-${action.quickLabel ?? "add"}` : "save-entry"; setActionLoading(loadingKey); setDataError(""); 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); setIsEntryModalOpen(false); } catch (error) { setDataError(appwriteErrorMessage(error)); } finally { setActionLoading(null); setLimitConfirmOpen(false); setPendingLimitAction(null); setLimitConfirmMessage(""); } } function requestEntrySave(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 persistEntry({ kind: "save", draft, editingId }); } async function saveEntry(draft: EntryDraft) { if (!user) return; requestEntrySave(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 persistEntry({ kind: "quick", draft, quickLabel: item.label }); } function confirmLimitOverride() { if (!pendingLimitAction) return; void persistEntry(pendingLimitAction); } 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 (
{showOnboarding && user && ( setShowOnboarding(false)} /> )} void importExcel(event.currentTarget.files?.[0])} /> void importJson(event.currentTarget.files?.[0])} />
setActiveView("settings")} />
{activeView === "overview" && user && ( void quickAdd(item)} onAdd={openNewEntry} onOpenCoach={(prompt) => { if (prompt) coachSession.queuePrompt(prompt); setActiveView("coach"); }} onOpenLogbook={() => setActiveView("logbook")} onOpenSettings={() => setActiveView("settings")} /> )} {activeView === "logbook" && ( { setEditingEntry(entry); setIsEntryModalOpen(true); }} onDelete={(id) => void deleteEntry(id)} /> )} {activeView === "trends" && ( void saveUserLimits(next)} /> )} {activeView === "coach" && user && ( )} {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={() => setShowOnboarding(true)} /> )}
{ setIsEntryModalOpen(false); setEditingEntry(null); }} onSave={(draft) => void saveEntry(draft)} /> 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, onOAuth, onSignup, }: { authError: string; busy: boolean; setupStatus: SetupStatus; shellStyle: CSSProperties; themeId: string; 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

Track intake, sync across devices.

{setupStatus.state !== "ok" && (
{setupStatus.message}
)}
{mode === "signup" && ( )} {authError && (
{authError}
)}
or
); } function AuthSignal({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) { return (
); } function CurrentThemeIndicator({ theme, onClick, }: { theme: AppTheme; onClick: () => void; }) { return ( ); } function ThemePicker({ themeId, onChange, }: { themeId: string; onChange: (id: string) => void; }) { const [category, setCategory] = useState("vocaloid"); const activeTheme = getThemeById(themeId); const visibleThemes = APP_THEMES.filter((theme) => theme.category === category); return (
{THEME_CATEGORIES.map((entry) => ( ))}
Primary
Surface
Chart
{visibleThemes.map((theme) => ( ))}

Current theme: {activeTheme.label}

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

{subtitle}

{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, user, coachSession, userLimits, limitCheck, onQuickAdd, onAdd, onOpenCoach, onOpenLogbook, onOpenSettings, }: { 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 }>; user: AuthUser; userLimits: UserLimits; limitCheck: LimitCheckResult; coachSession: CoachSession; onQuickAdd: (item: (typeof QUICK_ADDS)[number]) => void; onAdd: () => void; onOpenCoach: (prompt?: string) => void; onOpenLogbook: () => void; onOpenSettings: () => void; }) { const todaySpendRaw = limitCheck.todaySpend; const spendLimitDetail = userLimits.dailySpendLimit != null ? `${currency.format(todaySpendRaw)} of ${currency.format(userLimits.dailySpendLimit)} today` : `${dashboard.monthSpend} this month`; return (
onOpenCoach()} />
{limitCheck.violations.length ? (
) : null}
{chartData.length ? ( } /> ) : ( )} {recentEntries.length ? (
{recentEntries.map((entry) => ( ))}
) : ( )}
{insights.map((insight) => ( ))}
{flavourData.length ? ( {flavourData.map((entry) => ( ))} } /> ) : ( )}
); } function GreetingPanel({ dashboard, user, userLimits, limitCheck, onOpenCoach, }: { dashboard: Dashboard; user: AuthUser; userLimits: UserLimits; limitCheck: LimitCheckResult; onOpenCoach: (prompt?: string) => void; }) { const todayNumber = Number.parseFloat(dashboard.todayCans) || 0; const canLimit = userLimits.dailyCanLimit; const progress = canLimit ? Math.min(100, Math.round((todayNumber / canLimit) * 100)) : 0; const ringState = limitCheck.violations.includes("cans") ? "over" : canLimit && todayNumber >= canLimit * 0.75 ? "warn" : "ok"; const name = firstName(user); const greeting = buildDynamicGreeting({ name, todayCans: todayNumber, favouriteFlavour: dashboard.favouriteFlavour, currentStreak: Number.parseInt(dashboard.currentStreak, 10) || 0, todayCaffeineMg: Number.parseFloat(dashboard.todayCaffeine.replace(/[^\d.]/g, "")) || 0, allTimeCans: Number.parseFloat(dashboard.allTimeCans) || 0, dailyCanLimit: canLimit, limitCheck, }); const coachPrompts = [ { label: "Pace today's caffeine", prompt: "what does my red bull pattern say about today?", }, { label: "Sugar-free swap", prompt: "give me one lower-sugar swap based on my favourite flavour.", }, { label: "Weekly spend trend", prompt: "review my weekly spend trend and suggest one saving.", }, ]; return (
{dashboard.todayCans} {canLimit ? `of ${canLimit}` : "today"}

{greeting.headline}

{greeting.subline}

{coachPrompts.map((item) => ( ))}
); } function WellnessPill({ label, value }: { label: string; value: string }) { return (
{label} {value}
); } function TodayPanel({ dashboard, entries, userLimits, limitCheck, onAdd, }: { dashboard: Dashboard; entries: RedBullEntry[]; userLimits: UserLimits; limitCheck: LimitCheckResult; onAdd: () => void; }) { const limitSummary = [ userLimits.dailyCanLimit != null ? `${limitCheck.todayCans.toFixed(1)}/${userLimits.dailyCanLimit} cans` : null, userLimits.dailySpendLimit != null ? `${currency.format(limitCheck.todaySpend)} of ${currency.format(userLimits.dailySpendLimit)} spend` : null, ] .filter(Boolean) .join(" · "); return (

Today

{dashboard.todayCans}

cans logged

{limitSummary ?

{limitSummary}

: null}
{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, userLimits, 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[]; onFilterChange: (filters: Filters) => void; userLimits: UserLimits; onSaveLimits: (limits: UserLimits) => void; }) { return (
{chartData.length ? ( } /> ) : ( )}
{chartData.length ? ( } /> ) : ( )} {weekData.length ? ( } /> ) : ( )}
{flavourData.length ? ( {flavourData.map((entry) => ( ))} } /> ) : ( )}
{insights.map((insight) => ( ))}
); } function SpendingPredictionsCard({ entries, userLimits, onSaveLimits, }: { entries: RedBullEntry[]; userLimits: UserLimits; onSaveLimits?: (limits: UserLimits) => void; }) { const [projectionDays, setProjectionDays] = useState<7 | 30 | 90 | 365>(30); const now = new Date(); // Establish typical daily averages over last 30 calendar days (or all time if tracked less than 30 days) 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]); const trackingDays = useMemo(() => { const diffTime = Math.abs(now.getTime() - firstEntryDate.getTime()); return Math.max(1, Math.ceil(diffTime / (1000 * 60 * 60 * 24))); }, [firstEntryDate]); const activePeriodDays = Math.min(30, trackingDays); const stats = useMemo(() => { const cutoff = new Date(now.getTime() - activePeriodDays * 24 * 60 * 60 * 1000); const recent = entries.filter((e) => new Date(e.dateTime) >= cutoff); const totalSpend = recent.reduce((sum, e) => sum + e.cans * e.pricePerCan, 0); const totalCans = recent.reduce((sum, e) => sum + e.cans, 0); return { avgDailySpend: totalSpend / activePeriodDays, avgDailyCans: totalCans / activePeriodDays, hasData: entries.length > 0, }; }, [entries, activePeriodDays]); const projectionData = useMemo(() => { return Array.from({ length: projectionDays }).map((_, index) => { const day = index + 1; const dataPoint: any = { label: `Day ${day}`, "Current Path": Number((day * stats.avgDailySpend).toFixed(2)), "Optimal Path (-20%)": Number((day * stats.avgDailySpend * 0.8).toFixed(2)), }; if (userLimits.dailySpendLimit != null) { dataPoint["Daily Limit Path"] = Number((day * userLimits.dailySpendLimit).toFixed(2)); } return dataPoint; }); }, [projectionDays, stats, userLimits.dailySpendLimit]); if (!stats.hasData) { return ( ); } const projectedSpend = stats.avgDailySpend * projectionDays; const projectedCans = stats.avgDailyCans * projectionDays; const optimalSpend = projectedSpend * 0.8; const potentialSavings = projectedSpend - optimalSpend; const handleApplyOptimalLimit = () => { if (!onSaveLimits) return; const optimalDailySpendLimit = Math.round(stats.avgDailySpend * 0.8 * 100) / 100; onSaveLimits({ ...userLimits, dailySpendLimit: optimalDailySpendLimit, }); }; return (
{/* Toggle Range */}

Select projection window:

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

{currency.format(projectedSpend)}

~{oneDecimal.format(projectedCans)} cans logged
Optimal path (-20%)

{currency.format(optimalSpend)}

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

{currency.format(potentialSavings)}

{onSaveLimits && ( )}
{/* Projections Recharts AreaChart */}
`£${val}`} /> } /> {userLimits.dailySpendLimit != null && ( )}
The Optimal Path models a sustainable 20% reduction target, which fits guidelines for a healthy energy drink moderation pace. If a budget is active, the Limit Path displays the projection if you exhaust your daily limit budget completely every day.
); } function SettingsView({ activeTheme, dashboard, dataLoading, entries, notice, setupStatus, themeId, user, userLimits, limitCheck, actionLoading, onExportExcel, onImportExcel, onExportJson, onImportJson, onLogout, onReset, onThemeChange, onSaveLimits, onRerunOnboarding, }: { activeTheme: AppTheme; dashboard: Dashboard; dataLoading: boolean; entries: RedBullEntry[]; notice: string; setupStatus: SetupStatus; themeId: string; user: AuthUser | null; userLimits: UserLimits; limitCheck: LimitCheckResult; actionLoading: 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 (

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

{user?.email}

{dataLoading ?

{setupStatus.message}

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, userLimits, entries, onClose, onSave, }: { open: boolean; entry: RedBullEntry | null; flavours: Flavour[]; saving: boolean; userLimits: UserLimits; entries: RedBullEntry[]; 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(MATERIAL_ACCENTS.custom); 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 ?? MATERIAL_ACCENTS.custom); 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, ); const draftPreview = useMemo((): EntryDraft | null => { if (!open) return null; 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 ?? "manual", }; }, [ open, cans, pricePerCan, isOther, customFlavour, selectedFlavour, customAccent, numericSize, dateTime, notes, store, sugarFree, sizePreset, caffeineOverride, entry?.source, ]); const draftLimitCheck = useMemo(() => { if (!draftPreview) return null; return evaluateLimits(userLimits, entries, { draft: draftPreview, excludeEntryId: entry?.id }); }, [draftPreview, entries, entry?.id, userLimits]); function submit(event: FormEvent) { event.preventDefault(); if (!draftPreview) return; onSave(draftPreview); } return ( {open && (

Intake details

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

{draftLimitCheck?.violations.length ? (

{limitStatusMessage(draftLimitCheck.violations, draftLimitCheck, userLimits)} You can still save with confirmation.

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