perf: logbook memoization and pagination, debounced realtime, lazy exceljs
- Precompute per-entry spend/caffeine/sugar map; paginate ledger at 50 rows - Debounce realtime refetches (500ms trailing) and suppress during bulk import - Show import progress (Importing X of N) - Dynamic-import exceljs: entry chunk 1640 -> 700 kB - Skeleton placeholders for initial load on Overview/Trends/Logbook
This commit is contained in:
+40
-8
@@ -51,7 +51,7 @@ import {
|
||||
parseUserLimits,
|
||||
} from "./lib/userLimits";
|
||||
import { createExcelExport, downloadBlob, parseExcelImport } from "./lib/excel";
|
||||
import { groupByDay, groupByFlavour, groupByWeek, humanDateTime, makeId } from "./lib/metrics";
|
||||
import { buildEntryDerived, groupByDay, groupByFlavour, groupByWeek, humanDateTime, makeId } from "./lib/metrics";
|
||||
import { exportPayload, parseImport } from "./lib/storage";
|
||||
import { useToasts } from "./lib/toasts";
|
||||
import type {
|
||||
@@ -60,6 +60,7 @@ import type {
|
||||
EntryDraft,
|
||||
Filters,
|
||||
ImportPreview,
|
||||
ImportProgress,
|
||||
RedBullEntry,
|
||||
SetupStatus,
|
||||
UserLimits,
|
||||
@@ -101,6 +102,7 @@ function App() {
|
||||
const [busyAction, setBusyAction] = useState<string | null>(null);
|
||||
const [syncError, setSyncError] = useState("");
|
||||
const [importPreview, setImportPreview] = useState<ImportPreview | null>(null);
|
||||
const [importProgress, setImportProgress] = useState<ImportProgress | null>(null);
|
||||
const [userLimits, setUserLimits] = useState<UserLimits>({});
|
||||
const [limitConfirmOpen, setLimitConfirmOpen] = useState(false);
|
||||
const [limitConfirmMessage, setLimitConfirmMessage] = useState("");
|
||||
@@ -110,6 +112,8 @@ function App() {
|
||||
const { pushToast } = useToasts();
|
||||
const excelFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const jsonFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const importInFlightRef = useRef(false);
|
||||
const refreshDebounceRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, normaliseThemeId(themeId));
|
||||
@@ -203,16 +207,33 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (!user) return undefined;
|
||||
|
||||
// Trailing debounce: collapse bursts of row events (e.g. bulk imports) into one refetch.
|
||||
const scheduleRefresh = () => {
|
||||
if (refreshDebounceRef.current !== null) {
|
||||
window.clearTimeout(refreshDebounceRef.current);
|
||||
}
|
||||
refreshDebounceRef.current = window.setTimeout(() => {
|
||||
refreshDebounceRef.current = null;
|
||||
void refreshEntries(user.$id, false);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
// Bulk imports update local state directly; their row events need no refetch.
|
||||
if (importInFlightRef.current) return;
|
||||
if (event.payload?.userId === user.$id) scheduleRefresh();
|
||||
},
|
||||
);
|
||||
|
||||
return () => unsubscribe();
|
||||
return () => {
|
||||
if (refreshDebounceRef.current !== null) {
|
||||
window.clearTimeout(refreshDebounceRef.current);
|
||||
refreshDebounceRef.current = null;
|
||||
}
|
||||
unsubscribe();
|
||||
};
|
||||
}, [refreshEntries, user]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -227,6 +248,7 @@ function App() {
|
||||
() => mergedFlavours(entries.map((entry) => entry.flavour)),
|
||||
[entries],
|
||||
);
|
||||
const entryDerived = useMemo(() => buildEntryDerived(entries), [entries]);
|
||||
const entriesInView = useMemo(
|
||||
() => sortEntries(applyFilters(entries, filters)),
|
||||
[entries, filters],
|
||||
@@ -538,14 +560,18 @@ function App() {
|
||||
|
||||
setBusyAction("confirm-excel-import");
|
||||
setSyncError("");
|
||||
importInFlightRef.current = true;
|
||||
setImportProgress({ done: 0, total: drafts.length });
|
||||
try {
|
||||
const saved = await createEntries(user.$id, drafts);
|
||||
const saved = await createEntries(user.$id, drafts, (done, total) => setImportProgress({ done, total }));
|
||||
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 {
|
||||
importInFlightRef.current = false;
|
||||
setImportProgress(null);
|
||||
setBusyAction(null);
|
||||
}
|
||||
}
|
||||
@@ -567,12 +593,13 @@ function App() {
|
||||
setNotice("No new JSON entries found.");
|
||||
return;
|
||||
}
|
||||
importInFlightRef.current = true;
|
||||
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.`);
|
||||
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 {
|
||||
importInFlightRef.current = false;
|
||||
if (jsonFileInputRef.current) jsonFileInputRef.current.value = "";
|
||||
setBusyAction(null);
|
||||
}
|
||||
@@ -675,6 +702,7 @@ function App() {
|
||||
user={user}
|
||||
userLimits={userLimits}
|
||||
limitCheck={limitCheck}
|
||||
loading={dataLoading}
|
||||
onQuickAdd={(item) => void quickAdd(item)}
|
||||
onAdd={openNewEntry}
|
||||
onScan={openBarcodeScanner}
|
||||
@@ -687,8 +715,10 @@ function App() {
|
||||
<LogbookView
|
||||
entries={entriesInView}
|
||||
totalEntries={entries.length}
|
||||
entryDerived={entryDerived}
|
||||
filters={filters}
|
||||
flavours={allFlavours}
|
||||
loading={dataLoading}
|
||||
deletingId={deletingId}
|
||||
onFilterChange={setFilters}
|
||||
onAdd={openNewEntry}
|
||||
@@ -711,6 +741,7 @@ function App() {
|
||||
filters={filters}
|
||||
flavours={allFlavours}
|
||||
userLimits={userLimits}
|
||||
loading={dataLoading}
|
||||
onFilterChange={setFilters}
|
||||
onSaveLimits={(next) => void saveUserLimits(next)}
|
||||
/>
|
||||
@@ -781,6 +812,7 @@ function App() {
|
||||
<ImportPreviewModal
|
||||
busy={busyAction === "confirm-excel-import"}
|
||||
preview={importPreview}
|
||||
importProgress={importProgress}
|
||||
onClose={() => setImportPreview(null)}
|
||||
onConfirm={() => void confirmExcelImport()}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user