import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useParams, useSearchParams } from "react-router"; import type { QueryDefinition } from "@/api/types"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { Skeleton } from "@/components/ui/skeleton"; import LibraryHeader from "@/components/LibraryHeader"; import { useUserLibraries } from "@/hooks/queries/libraries"; import { useDocumentTitle } from "@/hooks/useDocumentTitle"; import LibraryRecommended from "./LibraryRecommended"; import LibraryBrowse from "./LibraryBrowse"; import LibraryCollections from "./LibraryCollections"; import { useLibraryPageStatePreference } from "@/hooks/queries/libraryPageState"; import { applySavedLibraryPageSearchParams, hasLibraryPageSearchParams, parseLibraryPageState, serializeLibraryPageSearchParams, updateLibraryPageSearchParams, type LibraryBrowseType, } from "./libraryPageSearchParams"; export default function LibraryPage() { const { libraryId } = useParams<{ libraryId: string }>(); const [searchParams, setSearchParams] = useSearchParams(); const { data: libraries, isLoading } = useUserLibraries(); const { isLoading: libraryPageStateLoading, preference: libraryPageStatePreference, rememberEnabled: rememberLibraryPageState, saveLibrarySearch, } = useLibraryPageStatePreference(); const savedStateHydratedLibraryIdRef = useRef(null); const applyingSavedSearchParamsRef = useRef(null); const applyingSavedSearchParamsLibraryIdRef = useRef(null); const id = Number(libraryId); const library = libraries?.find((l) => l.id === id); const libraryType = library?.type ?? ""; const savedLibrarySearch = Number.isFinite(id) && id > 0 ? libraryPageStatePreference.libraries[String(id)]?.search : undefined; const shouldApplySavedLibrarySearch = Boolean(libraryType) && Number.isFinite(id) && id > 0 && !libraryPageStateLoading && rememberLibraryPageState && savedStateHydratedLibraryIdRef.current !== id && savedLibrarySearch != null && !hasLibraryPageSearchParams(searchParams) && savedLibrarySearch !== serializeLibraryPageSearchParams(searchParams); const shouldWaitForSavedLibrarySearch = Boolean(libraryType) && Number.isFinite(id) && id > 0 && libraryPageStateLoading && savedStateHydratedLibraryIdRef.current !== id && !hasLibraryPageSearchParams(searchParams); const { activeTab, browseType, queryDefinition } = parseLibraryPageState( searchParams, libraryType, ); // Tracks whether LibraryRecommended is currently rendering a hero banner. // We can't derive this from layout metadata alone — a featured section can // still resolve to error/empty, in which case no hero is actually on screen // and overlay-mode header styling would be unreadable (especially on light // themes, where overlay assumes a dark backdrop behind it). const [hasRenderedHero, setHasRenderedHero] = useState(false); const handleHeroStateChange = useCallback((rendered: boolean) => { setHasRenderedHero(rendered); }, []); useDocumentTitle(library?.name ?? "Library"); useEffect(() => { if ( !libraryType || !Number.isFinite(id) || id <= 0 || libraryPageStateLoading || !rememberLibraryPageState || savedStateHydratedLibraryIdRef.current === id || !shouldApplySavedLibrarySearch ) { return; } savedStateHydratedLibraryIdRef.current = id; const nextSearchParams = applySavedLibraryPageSearchParams(searchParams, savedLibrarySearch); if (nextSearchParams.toString() !== searchParams.toString()) { applyingSavedSearchParamsRef.current = serializeLibraryPageSearchParams(nextSearchParams); applyingSavedSearchParamsLibraryIdRef.current = id; setSearchParams(nextSearchParams, { replace: true }); } }, [ id, libraryPageStateLoading, libraryType, rememberLibraryPageState, savedLibrarySearch, searchParams, setSearchParams, shouldApplySavedLibrarySearch, ]); useEffect(() => { if (!libraryType || shouldApplySavedLibrarySearch) { return; } if (applyingSavedSearchParamsLibraryIdRef.current !== id) { applyingSavedSearchParamsRef.current = null; applyingSavedSearchParamsLibraryIdRef.current = id; } const normalizedSearchParams = updateLibraryPageSearchParams( searchParams, { activeTab, browseType, queryDefinition }, libraryType, ); if (normalizedSearchParams.toString() !== searchParams.toString()) { setSearchParams(normalizedSearchParams, { replace: true }); } }, [ activeTab, browseType, queryDefinition, libraryType, searchParams, setSearchParams, shouldApplySavedLibrarySearch, ]); useEffect(() => { if ( !libraryType || !Number.isFinite(id) || id <= 0 || libraryPageStateLoading || !rememberLibraryPageState || shouldApplySavedLibrarySearch ) { return; } const normalizedSearchParams = updateLibraryPageSearchParams( searchParams, { activeTab, browseType, queryDefinition }, libraryType, ); if (normalizedSearchParams.toString() !== searchParams.toString()) { return; } const canonicalSearch = serializeLibraryPageSearchParams(normalizedSearchParams); if (applyingSavedSearchParamsRef.current != null) { if (applyingSavedSearchParamsRef.current === canonicalSearch) { applyingSavedSearchParamsRef.current = null; } return; } if (savedLibrarySearch !== canonicalSearch) { saveLibrarySearch(id, canonicalSearch); } }, [ activeTab, browseType, id, libraryPageStateLoading, libraryType, queryDefinition, rememberLibraryPageState, saveLibrarySearch, savedLibrarySearch, searchParams, shouldApplySavedLibrarySearch, ]); const handleTabChange = (value: string) => { const nextSearchParams = updateLibraryPageSearchParams( searchParams, { activeTab: value === "library" ? "library" : value === "collections" ? "collections" : "recommended", browseType, queryDefinition, }, libraryType, ); setSearchParams(nextSearchParams); }; const handleQueryDefinitionChange = (nextQueryDefinition: QueryDefinition) => { const nextSearchParams = updateLibraryPageSearchParams( searchParams, { activeTab: "library", browseType, queryDefinition: nextQueryDefinition, }, libraryType, ); setSearchParams(nextSearchParams); }; const handleBrowseTypeChange = ( nextBrowseType: LibraryBrowseType, nextQueryDefinition?: QueryDefinition, ) => { const nextSearchParams = updateLibraryPageSearchParams( searchParams, { activeTab: "library", browseType: nextBrowseType, queryDefinition: nextQueryDefinition ?? queryDefinition, }, libraryType, ); setSearchParams(nextSearchParams); }; if (isLoading || shouldWaitForSavedLibrarySearch || shouldApplySavedLibrarySearch) { return (
{Array.from({ length: 21 }).map((_, i) => (
))}
); } if (!library) { return (

This library is hidden or unavailable for your account.

Manage library visibility in Settings
); } const isRecommended = activeTab === "recommended"; // Only use transparent-overlay styling when a hero is actually on screen. // Without it, overlay styling (white text, no backdrop) would be illegible // against the normal page background — especially on light themes. const useOverlay = isRecommended && hasRenderedHero; return (
); }