import { useEffect } from "react"; import { useQuery } from "@tanstack/react-query"; import type { QueryKey } from "@tanstack/react-query"; import { Loader2, X } from "lucide-react"; import type { SearchTab } from "./types"; import { KEYWORD_RESEARCH_STALE_TIME_MS, buildKeywordResearchQueryKey, buildKeywordResearchRequest, keywordResearchQueryFn, } from "@/client/features/keywords/hooks/useKeywordResearchData"; import { getLanguageCode } from "@/client/features/keywords/locations"; import { getBacklinksOverview } from "@/serverFunctions/backlinks"; import { getDomainOverview } from "@/serverFunctions/domain"; export type { SearchTab } from "./types"; type Props = { activeTabId: string | null; projectId: string; tabs: SearchTab[]; onSelect: (tab: SearchTab) => void; onClose: (tabId: string) => void; onViewed: (tabId: string, when?: number) => void; }; type SearchTabQueryConfig = { queryKey: QueryKey; queryFn: () => Promise; staleTime?: number; gcTime?: number; }; export function SearchTabStrip({ activeTabId, projectId, tabs, onSelect, onClose, onViewed, }: Props) { if (tabs.length === 0) return null; return (
{tabs.map((tab) => { const active = tab.id === activeTabId; return (
); })}
); } function SearchTabStatus({ tab, projectId, active, onViewed, }: { tab: SearchTab; projectId: string; active: boolean; onViewed: (tabId: string, when?: number) => void; }) { const config = getSearchTabQueryConfig(projectId, tab); const query = useQuery({ queryKey: config.queryKey, queryFn: config.queryFn, enabled: false, select: () => null, notifyOnChangeProps: ["dataUpdatedAt", "fetchStatus", "status"], staleTime: config.staleTime, gcTime: config.gcTime, }); const isLoading = query.fetchStatus === "fetching"; const hasResult = query.dataUpdatedAt > 0; const hasError = query.status === "error"; const unviewed = !active && hasResult && (tab.viewedAt === null || tab.viewedAt < query.dataUpdatedAt); useEffect(() => { if (!active) return; if (!hasResult) return; if (tab.viewedAt !== null && tab.viewedAt >= query.dataUpdatedAt) return; onViewed(tab.id, query.dataUpdatedAt); }, [active, hasResult, onViewed, query.dataUpdatedAt, tab.id, tab.viewedAt]); const status = isLoading ? "loading" : hasError ? "error" : unviewed ? "unviewed" : "idle"; return ; } function SearchTabStatusIndicator({ status, }: { status: "idle" | "loading" | "unviewed" | "error"; }) { return ( {status === "loading" ? ( ) : status === "error" ? ( ) : status === "unviewed" ? ( ) : null} ); } function getSearchTabQueryConfig( projectId: string, tab: SearchTab, ): SearchTabQueryConfig { if (tab.input.type === "backlinks") { const input = tab.input; return { queryKey: ["backlinksOverview", projectId, input.scope, input.target], queryFn: () => getBacklinksOverview({ data: { projectId, target: input.target, scope: input.scope, hideSpam: false, }, }), }; } if (tab.input.type === "domain") { const input = tab.input; const trimmedDomain = input.domain.trim(); const languageCode = getLanguageCode(input.locationCode); return { queryKey: [ "domain-overview", projectId, trimmedDomain, input.subdomains, input.locationCode, languageCode, ], queryFn: () => getDomainOverview({ data: { projectId, domain: trimmedDomain, includeSubdomains: input.subdomains, locationCode: input.locationCode, languageCode, }, }), staleTime: 5 * 60_000, }; } const input = tab.input; const request = buildKeywordResearchRequest({ projectId, keywordInput: input.keyword, locationCode: input.locationCode, resultLimit: input.resultLimit, mode: input.mode, clickstream: input.clickstream, }); return { queryKey: buildKeywordResearchQueryKey(request), queryFn: () => { if (!request) throw new Error("Tab is missing a research request"); return keywordResearchQueryFn(request); }, staleTime: KEYWORD_RESEARCH_STALE_TIME_MS, gcTime: KEYWORD_RESEARCH_STALE_TIME_MS, }; }