Files
Red-Bull-Tracker/src/App.tsx
T
nh9961 50c729eae7 refactor: decompose monolithic App.tsx into per-component files
- Extract views to src/components/views/ (Overview, Logbook, Trends, Settings, Auth)
- Extract standalone components (EntryModal, ImportPreviewModal, ConfirmDialog,
  Sidebar, TopBar, MobileNav, EntryLedger, FiltersPanel, QuickAddPanel, etc.)
- Move shared helpers to src/lib (dashboard, filters, userDisplay) and static
  data to src/data (accents, quickAdds)
- App.tsx now orchestration-only (2971 -> 777 lines); no behavior change
2026-07-19 19:31:40 +00:00

778 lines
26 KiB
TypeScript

import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { flavourMeta, mergedFlavours } from "./data/flavours";
import { QUICK_ADDS, type QuickAddItem } from "./data/quickAdds";
import {
THEME_MODE_STORAGE_KEY,
THEME_STORAGE_KEY,
getThemeById,
getThemeTokens,
normaliseThemeId,
normaliseThemeMode,
readStoredThemeId,
readStoredThemeMode,
type ThemeMode,
} 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 { ConfirmDialog } from "./components/ConfirmDialog";
import { EntryModal } from "./components/EntryModal";
import { ImportPreviewModal } from "./components/ImportPreviewModal";
import { LoadingScreen } from "./components/LoadingScreen";
import { MobileNav } from "./components/MobileNav";
import { OnboardingScreen } from "./components/OnboardingScreen";
import { ShellBackdrop } from "./components/ShellBackdrop";
import { Sidebar } from "./components/Sidebar";
import { StatusRail } from "./components/StatusRail";
import { TopBar } from "./components/TopBar";
import { AuthView } from "./components/views/AuthView";
import { LogbookView } from "./components/views/LogbookView";
import { OverviewView } from "./components/views/OverviewView";
import { SettingsView } from "./components/views/SettingsView";
import { TrendsView } from "./components/views/TrendsView";
import { buildDashboard, buildInsights } from "./lib/dashboard";
import { applyFilters, DEFAULT_FILTERS } from "./lib/filters";
import {
evaluateLimits,
limitStatusMessage,
mergePrefsWithLimits,
parseUserLimits,
} from "./lib/userLimits";
import { createExcelExport, downloadBlob, parseExcelImport } from "./lib/excel";
import { groupByDay, groupByFlavour, groupByWeek, makeId } from "./lib/metrics";
import { exportPayload, parseImport } from "./lib/storage";
import type {
AppView,
AuthUser,
EntryDraft,
Filters,
ImportPreview,
RedBullEntry,
SetupStatus,
UserLimits,
} from "./types";
type PendingLimitAction = {
kind: "save" | "quick";
draft: EntryDraft;
editingId?: string;
quickLabel?: string;
};
function App() {
const [themeId, setThemeId] = useState(() => readStoredThemeId());
const [themeMode, setThemeMode] = useState<ThemeMode>(() => readStoredThemeMode());
const [systemDark, setSystemDark] = useState(
() => typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches,
);
const activeTheme = useMemo(() => getThemeById(themeId), [themeId]);
const resolvedMode: "light" | "dark" = themeMode === "system" ? (systemDark ? "dark" : "light") : themeMode;
const shellStyle = useMemo(() => themeTokensToStyle(getThemeTokens(themeId, resolvedMode)), [themeId, resolvedMode]);
const [user, setUser] = useState<AuthUser | null>(null);
const [authLoading, setAuthLoading] = useState(true);
const [authError, setAuthError] = useState("");
const [setupStatus, setSetupStatus] = useState<SetupStatus>({
state: "checking",
message: "Pinging Appwrite...",
});
const [entries, setEntries] = useState<RedBullEntry[]>([]);
const [filters, setFilters] = useState<Filters>(DEFAULT_FILTERS);
const [activeView, setActiveView] = useState<AppView>("overview");
const [isEntryModalOpen, setIsEntryModalOpen] = useState(false);
const [entryInitialDraft, setEntryInitialDraft] = useState<EntryDraft | null>(null);
const [editingEntry, setEditingEntry] = useState<RedBullEntry | null>(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<string | null>(null);
const [syncError, setSyncError] = useState("");
const [importPreview, setImportPreview] = useState<ImportPreview | null>(null);
const [userLimits, setUserLimits] = useState<UserLimits>({});
const [limitConfirmOpen, setLimitConfirmOpen] = useState(false);
const [limitConfirmMessage, setLimitConfirmMessage] = useState("");
const [pendingLimitAction, setPendingLimitAction] = useState<PendingLimitAction | null>(null);
const [setupOpen, setSetupOpen] = useState(false);
const excelFileInputRef = useRef<HTMLInputElement>(null);
const jsonFileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
localStorage.setItem(THEME_STORAGE_KEY, normaliseThemeId(themeId));
}, [themeId]);
useEffect(() => {
localStorage.setItem(THEME_MODE_STORAGE_KEY, themeMode);
}, [themeMode]);
useEffect(() => {
if (typeof window === "undefined") return undefined;
const media = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = (event: MediaQueryListEvent) => setSystemDark(event.matches);
setSystemDark(media.matches);
media.addEventListener("change", handleChange);
return () => media.removeEventListener("change", handleChange);
}, []);
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));
}
if (typeof currentUser.prefs.themeMode === "string" && currentUser.prefs.themeMode) {
setThemeMode(normaliseThemeMode(currentUser.prefs.themeMode));
}
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<Record<string, unknown>>(
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));
}
if (typeof currentUser.prefs.themeMode === "string" && currentUser.prefs.themeMode) {
setThemeMode(normaliseThemeMode(currentUser.prefs.themeMode));
}
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,
themeMode,
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: QuickAddItem) {
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 <LoadingScreen setupStatus={setupStatus} shellStyle={shellStyle} themeId={themeId} resolvedMode={resolvedMode} />;
}
if (!user) {
return (
<AuthView
authError={authError}
busy={busyAction === "auth"}
setupStatus={setupStatus}
shellStyle={shellStyle}
themeId={themeId}
resolvedMode={resolvedMode}
onLogin={login}
onSignup={signup}
/>
);
}
return (
<div
className="app-shell min-h-screen overflow-x-hidden"
data-theme={themeId}
data-mode={resolvedMode}
style={shellStyle}
>
{setupOpen && user && (
<OnboardingScreen
userName={user.name || undefined}
activeThemeId={themeId}
onThemeChange={setThemeId}
onSave={saveOnboarding}
onClose={() => setSetupOpen(false)}
initialLimits={userLimits}
/>
)}
<input
ref={excelFileInputRef}
className="hidden"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
onChange={(event) => void importExcel(event.currentTarget.files?.[0])}
/>
<input
ref={jsonFileInputRef}
className="hidden"
type="file"
accept="application/json"
onChange={(event) => void importJson(event.currentTarget.files?.[0])}
/>
<ShellBackdrop />
<div className="app-layout">
<Sidebar
activeView={activeView}
dataLoading={dataLoading}
notice={notice}
setupStatus={setupStatus}
user={user}
onAdd={openNewEntry}
onScan={openBarcodeScanner}
onChange={setActiveView}
onOpenSettings={() => setActiveView("settings")}
/>
<div className="app-content">
<TopBar
activeView={activeView}
busyAction={busyAction}
onAdd={openNewEntry}
onScan={openBarcodeScanner}
className={activeView === "overview" ? "top-app-bar--overview" : ""}
/>
<StatusRail busyAction={busyAction} syncError={syncError} setupStatus={setupStatus} />
<AnimatePresence mode="wait">
<motion.main
key={activeView}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
className="app-view"
>
{activeView === "overview" && (
<OverviewView
summary={summary}
entries={entries}
insights={insights}
quickAdds={QUICK_ADDS}
recentEntries={recentEntries}
chartData={chartData}
flavourData={flavourData}
user={user}
userLimits={userLimits}
limitCheck={limitCheck}
onQuickAdd={(item) => void quickAdd(item)}
onAdd={openNewEntry}
onScan={openBarcodeScanner}
onOpenLogbook={() => setActiveView("logbook")}
onOpenSettings={() => setActiveView("settings")}
/>
)}
{activeView === "logbook" && (
<LogbookView
entries={entriesInView}
totalEntries={entries.length}
filters={filters}
flavours={allFlavours}
onFilterChange={setFilters}
onAdd={openNewEntry}
onEdit={(entry) => {
setEditingEntry(entry);
setIsEntryModalOpen(true);
}}
onDelete={(id) => void deleteEntry(id)}
/>
)}
{activeView === "trends" && (
<TrendsView
chartData={chartData}
weekData={weekData}
flavourData={flavourData}
insights={insights}
entries={entriesInView}
filters={filters}
flavours={allFlavours}
userLimits={userLimits}
onFilterChange={setFilters}
onSaveLimits={(next) => void saveUserLimits(next)}
/>
)}
{activeView === "settings" && (
<SettingsView
activeTheme={activeTheme}
summary={summary}
dataLoading={dataLoading}
entries={entries}
notice={notice}
setupStatus={setupStatus}
themeId={themeId}
themeMode={themeMode}
user={user}
userLimits={userLimits}
limitCheck={limitCheck}
busyAction={busyAction}
onExportExcel={() => void exportExcel()}
onImportExcel={() => excelFileInputRef.current?.click()}
onExportJson={exportJson}
onImportJson={() => jsonFileInputRef.current?.click()}
onLogout={() => void logout()}
onReset={() => setIsResetOpen(true)}
onThemeChange={setThemeId}
onThemeModeChange={setThemeMode}
onSaveLimits={(next) => void saveUserLimits(next)}
onRerunOnboarding={() => setSetupOpen(true)}
/>
)}
</motion.main>
</AnimatePresence>
</div>
</div>
<MobileNav activeView={activeView} onChange={setActiveView} />
<EntryModal
entry={editingEntry}
initialDraft={entryInitialDraft}
flavours={allFlavours}
open={isEntryModalOpen}
saving={busyAction === "save-entry"}
onClose={() => {
setIsEntryModalOpen(false);
setEditingEntry(null);
setEntryInitialDraft(null);
}}
onSave={(draft) => void saveEntryDraft(draft)}
/>
<BarcodeScannerModal
busy={busyAction === "save-entry"}
flavours={allFlavours}
open={isBarcodeScannerOpen}
userId={user.$id}
onAddNow={addBarcodeDraft}
onClose={() => setIsBarcodeScannerOpen(false)}
onEditBeforeAdding={editBarcodeDraft}
/>
<ImportPreviewModal
busy={busyAction === "confirm-excel-import"}
preview={importPreview}
onClose={() => setImportPreview(null)}
onConfirm={() => void confirmExcelImport()}
/>
<ConfirmDialog
busy={busyAction === "reset"}
open={isResetOpen}
title="Delete all Appwrite entries?"
body="This removes every intake entry owned by your current Appwrite user. Export first if you want a backup."
confirmLabel="Delete all"
onCancel={() => setIsResetOpen(false)}
onConfirm={() => void resetAll()}
/>
<ConfirmDialog
busy={Boolean(busyAction && pendingLimitAction)}
open={limitConfirmOpen}
title="Over your limit?"
body={limitConfirmMessage || "This intake goes past one of your daily limits."}
confirmLabel="Log anyway"
onCancel={() => {
setLimitConfirmOpen(false);
setPendingLimitAction(null);
setLimitConfirmMessage("");
}}
onConfirm={confirmLimitOverride}
/>
</div>
);
}
function sortEntries(entries: RedBullEntry[]) {
return [...entries].sort((left, right) => new Date(right.dateTime).getTime() - new Date(left.dateTime).getTime());
}
export default App;