"use client"; import React from "react"; import { useRouter, useSearchParams } from "next/navigation"; import { RefreshCw } from "lucide-react"; import { useAuth } from "@/components/auth/authContext"; import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect"; import { searchPath } from "@/lib/frontend/routes"; import { parseSearchUrlState, serializeSearchUrlState, SEARCH_SCOPE, } from "@/lib/frontend/search/urlState"; import { normalizeSearchUrlStateForUser } from "@/lib/frontend/search/normalizeState"; import { mapSearchError } from "@/lib/frontend/search/errorMapping"; import { useSearchQuery } from "@/lib/frontend/search/useSearchQuery"; import { useSearchBranches, BRANCH_LIST_STATE, } from "@/lib/frontend/search/useSearchBranches"; import ExplorerPageShell from "@/components/explorer/ExplorerPageShell"; import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard"; import ForbiddenView from "@/components/system/ForbiddenView"; import { Button } from "@/components/ui/button"; import SearchForm from "@/components/search/SearchForm"; import SearchResults from "@/components/search/SearchResults"; function buildSearchHref({ routeBranch, state }) { const base = searchPath(routeBranch); const qs = serializeSearchUrlState(state); return qs ? `${base}?${qs}` : base; } export default function SearchPage({ branch: routeBranch }) { const router = useRouter(); const searchParams = useSearchParams(); const { status: authStatus, user } = useAuth(); const isAuthenticated = authStatus === "authenticated" && user; const isAdminDev = isAuthenticated && (user.role === "admin" || user.role === "dev"); // 1) URL -> parsed state (pure helper) const parsedUrlState = React.useMemo(() => { return parseSearchUrlState(searchParams, { routeBranch }); }, [searchParams, routeBranch]); // 2) Normalize for user role + route context (pure helper) const urlState = React.useMemo(() => { return normalizeSearchUrlStateForUser(parsedUrlState, { routeBranch, user, }); }, [parsedUrlState, routeBranch, user]); // 3) The identity of a first-page search (cursor intentionally excluded). const searchKey = React.useMemo(() => { return serializeSearchUrlState(urlState); }, [urlState]); // 4) Draft input (URL remains SoT for executed searches). const [qDraft, setQDraft] = React.useState(urlState.q || ""); React.useEffect(() => { setQDraft(urlState.q || ""); }, [urlState.q]); // 5) Admin/dev only: branches list for multi select (fail-open) const branchesQuery = useSearchBranches({ enabled: isAdminDev }); // 6) Data lifecycle (first page + load more) const query = useSearchQuery({ searchKey, urlState, routeBranch, user, limit: urlState.limit, }); // 7) Map errors to German UX copy const mappedError = React.useMemo( () => mapSearchError(query.error), [query.error] ); const mappedLoadMoreError = React.useMemo( () => mapSearchError(query.loadMoreError), [query.loadMoreError] ); // 8) Redirect when unauthenticated mid-request React.useEffect(() => { if (mappedError?.kind !== "unauthenticated") return; const next = typeof window !== "undefined" ? `${window.location.pathname}${window.location.search}` : searchPath(routeBranch); window.location.replace( buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next }) ); }, [mappedError?.kind, routeBranch]); // 9) URL write helpers (search is URL-driven) const pushStateToUrl = React.useCallback( (nextState) => { router.push(buildSearchHref({ routeBranch, state: nextState })); }, [router, routeBranch] ); const replaceStateToUrl = React.useCallback( (nextState) => { router.replace(buildSearchHref({ routeBranch, state: nextState })); }, [router, routeBranch] ); // 10) Handlers const handleSubmit = React.useCallback(() => { const nextState = { ...urlState, q: qDraft, branch: routeBranch, }; pushStateToUrl(nextState); }, [urlState, qDraft, routeBranch, pushStateToUrl]); const handleScopeChange = React.useCallback( (nextScope) => { if (!isAdminDev) return; const nextState = { ...urlState, scope: nextScope, branch: routeBranch, branches: nextScope === SEARCH_SCOPE.MULTI ? urlState.branches : [], }; replaceStateToUrl(nextState); }, [isAdminDev, urlState, routeBranch, replaceStateToUrl] ); const handleToggleBranch = React.useCallback( (branchId) => { if (!isAdminDev) return; const current = Array.isArray(urlState.branches) ? urlState.branches : []; const set = new Set(current); if (set.has(branchId)) set.delete(branchId); else set.add(branchId); const nextState = { ...urlState, scope: SEARCH_SCOPE.MULTI, branches: Array.from(set), }; replaceStateToUrl(nextState); }, [isAdminDev, urlState, replaceStateToUrl] ); const handleLimitChange = React.useCallback( (nextLimit) => { const nextState = { ...urlState, limit: nextLimit, branch: routeBranch, }; // Like scope changes: rerun based on URL state (executed query), not on draft. replaceStateToUrl(nextState); }, [urlState, routeBranch, replaceStateToUrl] ); // Forbidden stays consistent with Explorer UX. if (mappedError?.kind === "forbidden") { return ; } const actions = ( ); const resultsHeaderRight = urlState.q ? ( {urlState.q} ) : null; const resultsDescription = urlState.q ? `Niederlassung ${routeBranch}` : "Geben Sie einen Suchbegriff ein, um zu starten."; return ( ); }