feat(frontend): add createStore factory + central localStorage registry
- lib/store.ts: createStore<T> — one observable-value primitive (get/set/subscribe/use) replacing the hand-rolled listeners/subscribe/emit triad several modules each had. - lib/storage.ts: LS key registry (every siftlode.* key in one place), readMerged/ readJSON/writeJSON (the try/JSON.parse/merge/catch done once), and usePersistedState (the reactive persisted-string hook, generalizing usePersistedTab).
This commit is contained in:
parent
239a298992
commit
634921b35a
2 changed files with 113 additions and 0 deletions
80
frontend/src/lib/storage.ts
Normal file
80
frontend/src/lib/storage.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { useState } from "react";
|
||||
|
||||
// Central registry of every localStorage key the app uses. One place to see them all, rename
|
||||
// safely, and avoid collisions — instead of `"siftlode.x"` string literals scattered across
|
||||
// files. Parametric (per-user) keys are functions. All keys are namespaced `siftlode.*`.
|
||||
export const LS = {
|
||||
theme: "siftlode.theme",
|
||||
sidebarLayout: "siftlode.sidebarLayout",
|
||||
hints: "siftlode.hints",
|
||||
lang: "siftlode.lang",
|
||||
filters: "siftlode.filters",
|
||||
page: "siftlode.page",
|
||||
perfMode: "siftlode.perfMode",
|
||||
channelFilter: "siftlode.channelFilter",
|
||||
channelsView: "siftlode.channelsView",
|
||||
settingsTab: "siftlode.settingsTab",
|
||||
statsTab: "siftlode.statsTab",
|
||||
adminUsersTab: "siftlode.adminUsersTab",
|
||||
navCollapsed: "siftlode.navCollapsed",
|
||||
playlist: "siftlode.playlist",
|
||||
plSort: "siftlode.plSort",
|
||||
notifHistory: "siftlode.notifications",
|
||||
notifSettings: "siftlode.notifSettings",
|
||||
onboardingDismissed: "siftlode.onboarding.dismissed",
|
||||
seenVersion: "siftlode.seenVersion",
|
||||
chatDock: (userId: number) => `siftlode.chatDock.${userId}`,
|
||||
} as const;
|
||||
|
||||
// --- typed read/write helpers (do the JSON + try/catch once) ----------------------------
|
||||
|
||||
/** Read an object-shaped value merged over `defaults` (so missing/added keys are tolerated),
|
||||
* falling back to `defaults` on missing or corrupt data. */
|
||||
export function readMerged<T extends object>(key: string, defaults: T): T {
|
||||
try {
|
||||
return { ...defaults, ...JSON.parse(localStorage.getItem(key) || "{}") };
|
||||
} catch {
|
||||
return { ...defaults };
|
||||
}
|
||||
}
|
||||
|
||||
/** Read any JSON value (arrays/scalars), falling back on missing or corrupt data. */
|
||||
export function readJSON<T>(key: string, fallback: T): T {
|
||||
try {
|
||||
const raw = localStorage.getItem(key);
|
||||
return raw == null ? fallback : (JSON.parse(raw) as T);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist a value as JSON, swallowing quota/serialization errors. */
|
||||
export function writeJSON(key: string, value: unknown): void {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
} catch {
|
||||
/* ignore quota / serialization errors */
|
||||
}
|
||||
}
|
||||
|
||||
// --- reactive persisted string state ----------------------------------------------------
|
||||
|
||||
/** A `useState` whose string value is mirrored to localStorage, so it survives a reload (F5).
|
||||
* Validation is deferred to the caller (clamp at render) so it can be used before an
|
||||
* async-loaded option list is known, keeping hook order stable. Generalizes the per-component
|
||||
* "persisted active tab" pattern (Settings, Stats, admin pages, the channel filter…). */
|
||||
export function usePersistedState(
|
||||
key: string,
|
||||
fallback = "",
|
||||
): [string, (value: string) => void] {
|
||||
const [value, setValue] = useState<string>(() => localStorage.getItem(key) ?? fallback);
|
||||
const set = (next: string) => {
|
||||
setValue(next);
|
||||
try {
|
||||
localStorage.setItem(key, next);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
return [value, set];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue