refactor(frontend): adopt store/storage helpers; centralize persistence
- errorDialog + hints now use createStore (drop their bespoke listener arrays). - theme/sidebarLayout/notifications/App filters/Playlists plSort use readMerged/ readJSON/writeJSON instead of inline try/JSON.parse/catch. - Stats, SettingsPanel and App's channel filter/view tabs use usePersistedState (the 3 sites that reinvented usePersistedTab inline). - Every siftlode.* key now sourced from the LS registry (no scattered literals).
This commit is contained in:
parent
634921b35a
commit
472aba6480
15 changed files with 80 additions and 147 deletions
|
|
@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { Ban, Check, RotateCcw, Shield, Trash2, UserPlus, X } from "lucide-react";
|
||||
import { api, type AdminUserRow, type Invite, type Me } from "../lib/api";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { LS } from "../lib/storage";
|
||||
import Tooltip from "./Tooltip";
|
||||
import { useConfirm } from "./ConfirmProvider";
|
||||
import Tabs, { usePersistedTab } from "./Tabs";
|
||||
|
|
@ -14,7 +15,7 @@ import Tabs, { usePersistedTab } from "./Tabs";
|
|||
// The persisted-tab storage key + the access-requests tab id, exported so a notification's
|
||||
// "Review" link can pre-select that tab before navigating here (usePersistedTab reads the key
|
||||
// on mount, and this page mounts fresh on navigation).
|
||||
export const ADMIN_USERS_TAB_KEY = "siftlode.adminUsersTab";
|
||||
export const ADMIN_USERS_TAB_KEY = LS.adminUsersTab;
|
||||
export const ADMIN_USERS_ACCESS_TAB = "access";
|
||||
|
||||
export function focusAccessRequestsTab(): void {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
import { api, type Me } from "../lib/api";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { getUnreadCount, subscribe } from "../lib/notifications";
|
||||
import { LS } from "../lib/storage";
|
||||
import type { Page } from "../lib/urlState";
|
||||
import { type LangCode } from "../i18n";
|
||||
import AvatarImg from "./Avatar";
|
||||
|
|
@ -48,7 +49,7 @@ export default function NavSidebar({
|
|||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [collapsed, setCollapsed] = useState<boolean>(
|
||||
() => localStorage.getItem("siftlode.navCollapsed") === "1"
|
||||
() => localStorage.getItem(LS.navCollapsed) === "1"
|
||||
);
|
||||
const [acctOpen, setAcctOpen] = useState(false);
|
||||
const acctBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
|
@ -90,7 +91,7 @@ export default function NavSidebar({
|
|||
function toggleCollapsed() {
|
||||
setCollapsed((c) => {
|
||||
const next = !c;
|
||||
localStorage.setItem("siftlode.navCollapsed", next ? "1" : "0");
|
||||
localStorage.setItem(LS.navCollapsed, next ? "1" : "0");
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import {
|
|||
import { api, type Playlist, type Video } from "../lib/api";
|
||||
import { formatDuration } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { LS, readMerged, writeJSON } from "../lib/storage";
|
||||
import { useUndoable } from "../lib/useUndoable";
|
||||
import PlayerModal from "./PlayerModal";
|
||||
import UndoToolbar from "./UndoToolbar";
|
||||
|
|
@ -187,26 +188,18 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
|||
const confirm = useConfirm();
|
||||
// Persist the selected playlist so F5 keeps it (the de-URL refactor dropped it from the URL).
|
||||
const [selectedId, setSelectedId] = useState<number | null>(() => {
|
||||
const s = localStorage.getItem("siftlode.playlist");
|
||||
const s = localStorage.getItem(LS.playlist);
|
||||
return s ? Number(s) : null;
|
||||
});
|
||||
useEffect(() => {
|
||||
if (selectedId != null) localStorage.setItem("siftlode.playlist", String(selectedId));
|
||||
if (selectedId != null) localStorage.setItem(LS.playlist, String(selectedId));
|
||||
}, [selectedId]);
|
||||
const selectedRef = useRef<HTMLButtonElement | null>(null);
|
||||
const scrolledRef = useRef(false);
|
||||
// How the left playlist rail is ordered (persisted).
|
||||
const [plSort, setPlSort] = useState<PlSort>(() => {
|
||||
try {
|
||||
const s = localStorage.getItem("siftlode.plSort");
|
||||
if (s) return { ...PL_SORT_DEFAULT, ...JSON.parse(s) };
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return PL_SORT_DEFAULT;
|
||||
});
|
||||
const [plSort, setPlSort] = useState<PlSort>(() => readMerged(LS.plSort, PL_SORT_DEFAULT));
|
||||
useEffect(() => {
|
||||
localStorage.setItem("siftlode.plSort", JSON.stringify(plSort));
|
||||
writeJSON(LS.plSort, plSort);
|
||||
}, [plSort]);
|
||||
const [newName, setNewName] = useState("");
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||
import { Bell, Check, Monitor, RotateCcw, Save, Trash2, User } from "lucide-react";
|
||||
import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
|
||||
import { api, type Me } from "../lib/api";
|
||||
import { LS, usePersistedState } from "../lib/storage";
|
||||
import Avatar from "./Avatar";
|
||||
import { notify, type NotifSettings } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
|
@ -38,14 +39,6 @@ const TABS: { id: TabId; icon: typeof Monitor }[] = [
|
|||
{ id: "account", icon: User },
|
||||
];
|
||||
|
||||
// Persist the active tab so a reload (F5) keeps the user where they were instead of
|
||||
// snapping back to "appearance".
|
||||
const SETTINGS_TAB_KEY = "siftlode.settingsTab";
|
||||
function loadSettingsTab(): TabId {
|
||||
const v = localStorage.getItem(SETTINGS_TAB_KEY);
|
||||
return TABS.some((tabItem) => tabItem.id === v) ? (v as TabId) : "appearance";
|
||||
}
|
||||
|
||||
// Settings as a page (Design B): rendered in the main content area, not an overlay. It keeps
|
||||
// the frosted `.glass` surface so it still refracts the ambient backdrop. The page title is
|
||||
// shown by the Header; the tab rail stays as in-page sub-navigation.
|
||||
|
|
@ -59,11 +52,11 @@ export default function SettingsPanel({
|
|||
onOpenWizard: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [tab, setTabState] = useState<TabId>(loadSettingsTab);
|
||||
const setTab = (id: TabId) => {
|
||||
setTabState(id);
|
||||
localStorage.setItem(SETTINGS_TAB_KEY, id);
|
||||
};
|
||||
const [tabRaw, setTab] = usePersistedState(LS.settingsTab, "appearance");
|
||||
// Clamp at render so a stale/removed tab id falls back to Appearance.
|
||||
const tab: TabId = TABS.some((tabItem) => tabItem.id === tabRaw)
|
||||
? (tabRaw as TabId)
|
||||
: "appearance";
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-3xl w-full mx-auto">
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import { History, Pause, Play, RefreshCw } from "lucide-react";
|
|||
import { api, type AdminQuotaRow, type Me } from "../lib/api";
|
||||
import { formatEta, quotaActionLabel } from "../lib/format";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { LS, usePersistedState } from "../lib/storage";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
const RANGES = [7, 30, 90] as const;
|
||||
const STATS_TAB_KEY = "siftlode.statsTab";
|
||||
type StatsTab = "overview" | "system";
|
||||
|
||||
// Usage & stats page. "Overview" is per-user (sync status + your own API usage + actions);
|
||||
|
|
@ -17,14 +17,9 @@ type StatsTab = "overview" | "system";
|
|||
export default function Stats({ me }: { me: Me }) {
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = me.role === "admin";
|
||||
const [tab, setTabState] = useState<StatsTab>(() => {
|
||||
const v = localStorage.getItem(STATS_TAB_KEY);
|
||||
return v === "system" && isAdmin ? "system" : "overview";
|
||||
});
|
||||
const setTab = (id: StatsTab) => {
|
||||
setTabState(id);
|
||||
localStorage.setItem(STATS_TAB_KEY, id);
|
||||
};
|
||||
const [tabRaw, setTab] = usePersistedState(LS.statsTab, "overview");
|
||||
// Clamp at render: only admins may land on the System tab (covers a stale stored value).
|
||||
const tab: StatsTab = tabRaw === "system" && isAdmin ? "system" : "overview";
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-4xl mx-auto">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { usePersistedState } from "../lib/storage";
|
||||
|
||||
// Reusable horizontal pill tab-rail for in-page sub-navigation (Configuration, Users & roles…).
|
||||
// The active tab is persisted to localStorage so a reload (F5) keeps the user where they were,
|
||||
|
|
@ -10,16 +10,9 @@ export interface TabDef {
|
|||
badge?: number; // optional count pill (e.g. pending requests); hidden when 0/undefined
|
||||
}
|
||||
|
||||
/** Persisted active-tab state. Validation is deferred to the caller (it clamps to a valid id at
|
||||
* render) so this can be called before an async-loaded tab list is known, keeping hook order stable. */
|
||||
export function usePersistedTab(storageKey: string, fallback = ""): [string, (id: string) => void] {
|
||||
const [tab, setTabState] = useState<string>(() => localStorage.getItem(storageKey) ?? fallback);
|
||||
const setTab = (id: string) => {
|
||||
setTabState(id);
|
||||
localStorage.setItem(storageKey, id);
|
||||
};
|
||||
return [tab, setTab];
|
||||
}
|
||||
/** Persisted active-tab state — the canonical helper now lives in lib/storage as
|
||||
* `usePersistedState`; re-exported here under its original name for existing call sites. */
|
||||
export const usePersistedTab = usePersistedState;
|
||||
|
||||
export default function Tabs({
|
||||
tabs,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue