feat(pwa): web manifest, icons, read-through entry cache

- Add manifest.webmanifest, generated 192/512 icons, theme-color, apple-touch-icon
- Cache entries in localStorage per user; hydrate on load before network refresh
- Single info toast when offline on cached data; cache cleared on logout
- Fix inaccurate local-first meta description
This commit is contained in:
nh9961
2026-07-19 21:19:43 +00:00
parent 767967f25d
commit 7f64eec207
6 changed files with 113 additions and 16 deletions
+35 -15
View File
@@ -51,6 +51,7 @@ import {
} from "./lib/userLimits";
import { createExcelExport, downloadBlob, parseExcelImport } from "./lib/excel";
import { buildEntryDerived, groupByDay, groupByFlavour, groupByWeek, humanDateTime, makeId } from "./lib/metrics";
import { clearCachedEntries, readCachedEntries, writeCachedEntries } from "./lib/entryCache";
import { buildJsonImportPreview, exportPayload, parseImport } from "./lib/storage";
import { useToasts } from "./lib/toasts";
import type {
@@ -113,6 +114,7 @@ function App() {
const jsonFileInputRef = useRef<HTMLInputElement>(null);
const importInFlightRef = useRef(false);
const refreshDebounceRef = useRef<number | null>(null);
const cacheHydratedRef = useRef(false);
useEffect(() => {
localStorage.setItem(THEME_STORAGE_KEY, normaliseThemeId(themeId));
@@ -131,21 +133,31 @@ function App() {
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);
}
}, []);
const refreshEntries = useCallback(
async (userId: string, showLoader = true) => {
if (showLoader) setDataLoading(true);
setSyncError("");
try {
const remoteEntries = await listEntries(userId);
setEntries(sortEntries(remoteEntries));
writeCachedEntries(userId, remoteEntries);
cacheHydratedRef.current = false;
setNotice(`Synced ${remoteEntries.length} Appwrite entr${remoteEntries.length === 1 ? "y" : "ies"}.`);
} catch (error) {
if (cacheHydratedRef.current) {
// Cached entries are on screen, so one info toast replaces the generic error toast.
pushToast({ tone: "info", message: "Couldn't reach the server — showing saved data" });
} else {
const message = appwriteErrorMessage(error);
setSyncError(message);
setNotice("Appwrite sync failed.");
}
} finally {
if (showLoader) setDataLoading(false);
}
},
[pushToast],
);
useEffect(() => {
let mounted = true;
@@ -197,9 +209,16 @@ function App() {
useEffect(() => {
if (!user) {
cacheHydratedRef.current = false;
setEntries([]);
return;
}
const cachedEntries = readCachedEntries(user.$id);
cacheHydratedRef.current = cachedEntries !== null;
if (cachedEntries) {
// Render the last synced entries immediately; the refresh below replaces them.
setEntries(sortEntries(cachedEntries));
}
void refreshEntries(user.$id);
}, [refreshEntries, user]);
@@ -314,6 +333,7 @@ function App() {
setSyncError("");
try {
await account.deleteSession({ sessionId: "current" });
if (user) clearCachedEntries(user.$id);
setUser(null);
setEntries([]);
setNotice("Logged out.");