/* eslint-disable max-lines, max-lines-per-function -- Domain Overview keeps page-only orchestration colocated to avoid fake indirection. */ import { useCallback, useEffect, useMemo, useRef, type FormEvent } from "react"; import { useForm, useStore } from "@tanstack/react-form"; import { ArrowLeft } from "lucide-react"; import { toast } from "sonner"; import { DEFAULT_DOMAIN_KEYWORDS_PAGE_SIZE, type DomainSearchParams, } from "@/types/schemas/domain"; import { DEFAULT_LOCATION_CODE, LOCATIONS, getLanguageCode, isSupportedLocationCode, } from "@/client/features/keywords/locations"; import { useDomainSearchHistory } from "@/client/hooks/useDomainSearchHistory"; import type { DomainSearchHistoryItem } from "@/client/hooks/useDomainSearchHistory"; import { getDomainSearchChangeValidationErrors, getDomainSearchValidationErrors, } from "@/client/features/domain/domainSearchValidation"; import { useDomainOverviewQuery } from "@/client/features/domain/hooks/useDomainOverviewQuery"; import { DomainOverviewLoadingState } from "@/client/features/domain/components/DomainOverviewLoadingState"; import { DomainHistorySection } from "@/client/features/domain/components/DomainHistorySection"; import { DomainSearchCard } from "@/client/features/domain/components/DomainSearchCard"; import { KeywordsTab } from "@/client/features/domain/components/KeywordsTab"; import { PagesTab } from "@/client/features/domain/components/PagesTab"; import { StatCard } from "@/client/features/domain/components/StatCard"; import { SearchTabStrip } from "@/client/features/search-tabs/SearchTabStrip"; import type { SearchTabInput } from "@/client/features/search-tabs/types"; import { useSearchTabNavigation } from "@/client/features/search-tabs/useSearchTabNavigation"; import { formatMetric, getDefaultSortOrder, normalizeDomainTarget, toSortOrderSearchParam, toSortSearchParam, } from "@/client/features/domain/utils"; import { createFormValidationErrors, shouldValidateFieldOnChange, } from "@/client/lib/forms"; import { buildDomainFiltersClearSearchUpdate } from "@/client/features/domain/domainFilterUtils"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { captureClientEvent } from "@/client/lib/posthog"; import type { DomainOverviewRouteState } from "@/client/features/domain/domainRouteState"; import type { DomainActiveTab, DomainSortMode, SortOrder, } from "@/client/features/domain/types"; type Props = { projectId: string; routeState: DomainOverviewRouteState; navigate: (args: { search: (prev: Record) => Record; replace: boolean; }) => void; onShowRecentSearches: () => void; }; type DomainNavigate = Props["navigate"]; type DomainSearchUpdate = Partial; const KEYWORDS_ONLY_SORTS: ReadonlySet = new Set([ "rank", "score", "cpc", ]); function getSortSearchUpdate( nextSort: DomainSortMode, nextOrder: SortOrder, ): DomainSearchUpdate { return { sort: toSortSearchParam(nextSort), order: toSortOrderSearchParam(nextSort, nextOrder), page: undefined, }; } function getLocationSearchUpdate( nextLocationCode: number, ): DomainSearchUpdate | null { if (!isSupportedLocationCode(nextLocationCode)) return null; return { loc: nextLocationCode === DEFAULT_LOCATION_CODE ? undefined : nextLocationCode, page: undefined, }; } function getPageSearchUpdate(nextPage: number): DomainSearchUpdate { const safe = Math.max(1, Math.floor(nextPage)); return { page: safe === 1 ? undefined : safe }; } function getPageSizeSearchUpdate(nextSize: number): DomainSearchUpdate { return { size: nextSize === DEFAULT_DOMAIN_KEYWORDS_PAGE_SIZE ? undefined : nextSize, page: undefined, }; } function getTabSearchUpdate( nextTab: DomainActiveTab, currentSort: DomainSortMode, ): DomainSearchUpdate { if (nextTab === "keywords") { return { tab: undefined, page: undefined }; } const fallbackSortNeeded = KEYWORDS_ONLY_SORTS.has(currentSort); const update: DomainSearchUpdate = { tab: "pages", page: undefined, }; if (fallbackSortNeeded) { update.sort = "traffic"; update.order = getDefaultSortOrder("traffic"); } return update; } function getHistorySearchUpdate( item: DomainSearchHistoryItem, ): DomainSearchUpdate { const historyLocation = item.locationCode != null && isSupportedLocationCode(item.locationCode) ? item.locationCode : DEFAULT_LOCATION_CODE; return { ...buildDomainFiltersClearSearchUpdate(), domain: item.domain, subdomains: item.subdomains ? undefined : false, sort: toSortSearchParam(item.sort), order: undefined, tab: item.tab === "keywords" ? undefined : item.tab, loc: historyLocation === DEFAULT_LOCATION_CODE ? undefined : historyLocation, size: undefined, }; } function getSearchSubmitUpdate({ domain, subdomains, sort, locationCode, currentOrder, activeTab, }: { domain: string; subdomains: boolean; sort: DomainSortMode; locationCode: number; currentOrder: SortOrder; activeTab: DomainActiveTab; }): DomainSearchUpdate { return { ...buildDomainFiltersClearSearchUpdate(), domain, subdomains: subdomains ? undefined : false, sort: toSortSearchParam(sort), order: toSortOrderSearchParam(sort, currentOrder), tab: activeTab === "keywords" ? undefined : activeTab, loc: locationCode === DEFAULT_LOCATION_CODE ? undefined : locationCode, size: undefined, }; } function useDomainOverviewState({ navigate, routeState, projectId, }: { navigate: DomainNavigate; routeState: DomainOverviewRouteState; projectId: string; }) { const lastTrackedKey = useRef(""); const { history, isLoaded: historyLoaded, addSearch, removeHistoryItem, } = useDomainSearchHistory(projectId); const setSearchParams = useCallback( (updates: DomainSearchUpdate) => { navigate({ search: (prev) => ({ ...prev, ...updates }), replace: true, }); }, [navigate], ); const applySort = useCallback( (nextSort: DomainSortMode, nextOrder: SortOrder) => { setSearchParams(getSortSearchUpdate(nextSort, nextOrder)); }, [setSearchParams], ); const applyLocationChange = useCallback( (nextLocationCode: number) => { const update = getLocationSearchUpdate(nextLocationCode); if (update) setSearchParams(update); }, [setSearchParams], ); const handleSortColumnClick = useCallback( (nextSort: DomainSortMode) => { const nextOrder = nextSort === routeState.sort ? routeState.order === "asc" ? "desc" : "asc" : getDefaultSortOrder(nextSort); applySort(nextSort, nextOrder); }, [applySort, routeState.order, routeState.sort], ); const goToPage = useCallback( (nextPage: number) => { setSearchParams(getPageSearchUpdate(nextPage)); }, [setSearchParams], ); const setPageSize = useCallback( (nextSize: number) => { setSearchParams(getPageSizeSearchUpdate(nextSize)); }, [setSearchParams], ); const handleTabChange = useCallback( (nextTab: DomainActiveTab) => { setSearchParams(getTabSearchUpdate(nextTab, routeState.sort)); }, [routeState.sort, setSearchParams], ); const handleHistorySelect = useCallback( (item: DomainSearchHistoryItem) => { setSearchParams(getHistorySearchUpdate(item)); }, [setSearchParams], ); const languageCode = getLanguageCode(routeState.locationCode); const overviewQuery = useDomainOverviewQuery({ projectId, domain: routeState.domain, includeSubdomains: routeState.subdomains, locationCode: routeState.locationCode, languageCode, }); const overview = overviewQuery.data ?? null; const isLoading = overviewQuery.isLoading; const controlsForm = useForm({ defaultValues: { domain: routeState.domain, subdomains: routeState.subdomains, sort: routeState.sort, locationCode: routeState.locationCode, }, validators: { onChange: ({ formApi, value }) => getDomainSearchChangeValidationErrors( value, shouldValidateFieldOnChange(formApi, "domain"), formApi.state.submissionAttempts > 0, ), onSubmit: ({ value }) => getDomainSearchValidationErrors(value), }, onSubmit: ({ formApi, value }) => { const target = normalizeDomainTarget(value.domain); if (!target) return; formApi.setFieldValue("domain", target); setSearchParams( getSearchSubmitUpdate({ domain: target, subdomains: value.subdomains, sort: value.sort, locationCode: value.locationCode, currentOrder: routeState.order, activeTab: routeState.tab, }), ); }, }); useEffect(() => { controlsForm.reset({ domain: routeState.domain, subdomains: routeState.subdomains, sort: routeState.sort, locationCode: routeState.locationCode, }); }, [ controlsForm, routeState.domain, routeState.locationCode, routeState.sort, routeState.subdomains, ]); useEffect(() => { controlsForm.setErrorMap({ onSubmit: overviewQuery.error ? createFormValidationErrors({ form: getStandardErrorMessage( overviewQuery.error, "Lookup failed.", ), }) : undefined, }); }, [controlsForm, overviewQuery.error]); useEffect(() => { if (!overviewQuery.isSuccess || !overview) return; const key = `${routeState.domain}|${routeState.subdomains}|${routeState.locationCode}`; if (lastTrackedKey.current === key) return; lastTrackedKey.current = key; captureClientEvent("domain_overview:search_complete", { sort_mode: routeState.sort, include_subdomains: routeState.subdomains, result_count: overview.organicKeywords ?? 0, location_code: routeState.locationCode, }); addSearch({ domain: routeState.domain, subdomains: routeState.subdomains, sort: routeState.sort, tab: routeState.tab, locationCode: routeState.locationCode, }); if (!overview.hasData) { toast.info("Not enough data for this domain"); } }, [ addSearch, overview, overviewQuery.isSuccess, routeState.domain, routeState.locationCode, routeState.sort, routeState.subdomains, routeState.tab, ]); useEffect(() => { if (routeState.domain.trim() !== "") return; lastTrackedKey.current = ""; }, [routeState.domain]); const controlsLocationCode = useStore( controlsForm.store, (s) => s.values.locationCode, ); const canSaveKeywords = useMemo( () => controlsLocationCode === routeState.locationCode && overview !== null && overview.hasData, [controlsLocationCode, overview, routeState.locationCode], ); const handleSearchSubmit = useCallback( (event: FormEvent) => { event.preventDefault(); void controlsForm.handleSubmit(); }, [controlsForm], ); return { controlsForm, isLoading, overview, canSaveKeywords, history, historyLoaded, removeHistoryItem, languageCode, setSearchParams, applySort, applyLocationChange, handleTabChange, handleSortColumnClick, handleHistorySelect, handleSearchSubmit, goToPage, setPageSize, }; } export type DomainOverviewControlsForm = ReturnType< typeof useDomainOverviewState >["controlsForm"]; export function DomainOverviewPage({ projectId, routeState, navigate, onShowRecentSearches, }: Props) { const state = useDomainOverviewState({ navigate, routeState, projectId }); const urlTabInput = useMemo(() => { if (routeState.domain.trim() === "") return null; return { type: "domain", domain: routeState.domain, subdomains: routeState.subdomains, locationCode: routeState.locationCode, }; }, [routeState.domain, routeState.locationCode, routeState.subdomains]); const navigateToSearchTab = useCallback( (input: SearchTabInput | null) => { if (input?.type !== "domain") { navigate({ search: () => ({}), replace: true, }); return; } navigate({ search: (prev) => ({ ...prev, ...buildDomainFiltersClearSearchUpdate(), domain: input.domain, subdomains: input.subdomains ? undefined : false, sort: undefined, order: undefined, tab: undefined, page: undefined, loc: input.locationCode === DEFAULT_LOCATION_CODE ? undefined : input.locationCode, size: undefined, }), replace: true, }); }, [navigate], ); const searchTabs = useSearchTabNavigation({ storageKey: `domain:${projectId}`, urlInput: urlTabInput, getLabel: useCallback((input) => { if (input.type !== "domain") return ""; const locationSuffix = input.locationCode === DEFAULT_LOCATION_CODE ? "" : ` ${LOCATIONS[input.locationCode] ?? input.locationCode}`; return `${input.domain}${locationSuffix}`; }, []), navigateToInput: navigateToSearchTab, }); const handleSearchSubmit = useCallback( (event: FormEvent) => { const values = state.controlsForm.state.values; const target = normalizeDomainTarget(values.domain); if (!target) { state.handleSearchSubmit(event); return; } const nextTabInput: SearchTabInput = { type: "domain", domain: target, subdomains: values.subdomains, locationCode: values.locationCode, }; if (!searchTabs.canOpenTab(nextTabInput)) { event.preventDefault(); state.controlsForm.setErrorMap({ onSubmit: createFormValidationErrors({ fields: { domain: `Close a tab to open more searches (max ${searchTabs.limit}).`, }, }), }); return; } state.handleSearchSubmit(event); }, [searchTabs, state], ); const tabControls = routeState.domain ? (
) : null; return (

Domain Overview

Analyze any domain's SEO profile: traffic, keywords, and backlinks.

state.applySort(sort, getDefaultSortOrder(sort)) } onLocationChange={(locationCode) => state.applyLocationChange(locationCode) } /> {state.isLoading ? ( <> {tabControls} ) : state.overview === null ? (
) : ( <> {tabControls}
{!state.overview.hasData ? (
Not enough data for this domain yet. Try another domain or include subdomains.
) : null}
{routeState.tab === "keywords" ? ( ) : ( )}
)}
); }