Extend lib/queryKeys.ts to the remaining ~34 key roots and migrate every remaining literal (queryKey: [...] AND the positional useLiveQuery keys) in 36 files. All ~200 React Query keys across the app now route through the factory; zero raw key literals remain outside queryKeys.ts. The heterogeneous composite plex keys (plex-library/facets/collections/ playlists carry a filters object, a ratingKeys array, or a union/group discriminator) take a variadic pass-through — the factory centralizes the ROOT string; single-id keys stay precisely typed. Nullable keys (thread, playlist, ytSearch) accept null so a null segment is preserved. Pure substitution — byte-identical key arrays, so TanStack prefix matching is unchanged. Gate green: typecheck (app+node+e2e), eslint (0 err), prettier, 68 vitest. Runtime-verified on a fresh dev server: Feed + Plex modules load, feed/facets/tags/my-status/plex-library/plex-facets/ plex-collections/plex-playlists all 200, full UI renders.
74 lines
2.7 KiB
TypeScript
74 lines
2.7 KiB
TypeScript
import {
|
|
createContext,
|
|
lazy,
|
|
Suspense,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { qk } from "../lib/queryKeys";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { api } from "../lib/api";
|
|
import { shouldAutoOpenOnboarding } from "../lib/onboarding";
|
|
|
|
const OnboardingWizard = lazy(() => import("./OnboardingWizard"));
|
|
|
|
// Owns the onboarding wizard's open state (and the wizard render itself), lifted out of App so any
|
|
// module can trigger "connect YouTube" via a hook instead of an onOpenWizard prop threaded through
|
|
// the tree. Mirrors the other split-context providers; composed in main.tsx around <App/>.
|
|
//
|
|
// The wizard auto-opens on first login for accounts that still need to connect YouTube (derived from
|
|
// granted scopes + storage flags, so it's stable across the OAuth round-trip) and is reopenable from
|
|
// Settings / the notify "reconnect" action. It reads `me` as a passive qk.me() observer (App owns the
|
|
// fetching query), so it re-runs the auto-open check when the account resolves or its grants change.
|
|
|
|
interface WizardActions {
|
|
openWizard: () => void;
|
|
closeWizard: () => void;
|
|
}
|
|
|
|
// Only the actions are shared — the open flag stays local to the provider (it renders the wizard
|
|
// itself), so no consumer needs to subscribe to it.
|
|
const ActionsContext = createContext<WizardActions>({
|
|
openWizard: () => {},
|
|
closeWizard: () => {},
|
|
});
|
|
|
|
export function useWizard(): WizardActions {
|
|
return useContext(ActionsContext);
|
|
}
|
|
|
|
export function WizardProvider({ children }: { children: ReactNode }) {
|
|
const [open, setOpen] = useState(false);
|
|
// Passive observer of the account App fetches (enabled:false → reads cache, re-renders on update).
|
|
const { data: me } = useQuery({ queryKey: qk.me(), queryFn: api.me, enabled: false });
|
|
|
|
const openWizard = useCallback(() => setOpen(true), []);
|
|
const closeWizard = useCallback(() => setOpen(false), []);
|
|
|
|
// First-login onboarding: prompt the user to connect YouTube (and resume the flow after each
|
|
// consent redirect). Re-checked when the account or its grants change.
|
|
useEffect(() => {
|
|
if (me && shouldAutoOpenOnboarding(me)) setOpen(true);
|
|
}, [me?.id, me?.can_read, me?.can_write]);
|
|
|
|
const actions = useMemo<WizardActions>(
|
|
() => ({ openWizard, closeWizard }),
|
|
[openWizard, closeWizard],
|
|
);
|
|
|
|
return (
|
|
<ActionsContext.Provider value={actions}>
|
|
{children}
|
|
{/* The wizard is a full-screen overlay; the demo account can't connect YouTube, so it never
|
|
opens there. Lazy — its chunk loads only when first opened. */}
|
|
<Suspense fallback={null}>
|
|
{open && me && !me.is_demo && <OnboardingWizard me={me} onClose={closeWizard} />}
|
|
</Suspense>
|
|
</ActionsContext.Provider>
|
|
);
|
|
}
|