Share search tab queueing across Backlinks, Domain Overview, and Keyword Research (#207)
This commit is contained in:
parent
01ecf11415
commit
aebc779b93
@ -84,7 +84,6 @@ export function BacklinksPage({
|
|||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-4 pb-24 overflow-auto md:px-6 md:py-6 md:pb-8">
|
<div className="px-4 py-4 pb-24 overflow-auto md:px-6 md:py-6 md:pb-8">
|
||||||
<div className="mx-auto max-w-7xl space-y-4">
|
<div className="mx-auto max-w-7xl space-y-4">
|
||||||
@ -102,11 +101,6 @@ export function BacklinksPage({
|
|||||||
<BacklinksSearchCard
|
<BacklinksSearchCard
|
||||||
errorMessage={overviewErrorMessage}
|
errorMessage={overviewErrorMessage}
|
||||||
initialValues={searchCardInitialValues}
|
initialValues={searchCardInitialValues}
|
||||||
isFetching={
|
|
||||||
overviewQuery.isFetching ||
|
|
||||||
referringDomainsQuery.isFetching ||
|
|
||||||
topPagesQuery.isFetching
|
|
||||||
}
|
|
||||||
canOpenSearch={(values) =>
|
canOpenSearch={(values) =>
|
||||||
searchTabs.canOpenTab(toBacklinksTabInput(values))
|
searchTabs.canOpenTab(toBacklinksTabInput(values))
|
||||||
}
|
}
|
||||||
@ -147,6 +141,7 @@ export function BacklinksPage({
|
|||||||
tabs: searchTabs.tabs,
|
tabs: searchTabs.tabs,
|
||||||
onSelect: searchTabs.selectTab,
|
onSelect: searchTabs.selectTab,
|
||||||
onClose: searchTabs.closeTab,
|
onClose: searchTabs.closeTab,
|
||||||
|
onViewed: searchTabs.markTabViewed,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,6 +52,7 @@ type BacklinksBodyProps = {
|
|||||||
tabs: SearchTab[];
|
tabs: SearchTab[];
|
||||||
onSelect: (tab: SearchTab) => void;
|
onSelect: (tab: SearchTab) => void;
|
||||||
onClose: (tabId: string) => void;
|
onClose: (tabId: string) => void;
|
||||||
|
onViewed: (tabId: string, when?: number) => void;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,10 +106,12 @@ export function BacklinksBody({
|
|||||||
);
|
);
|
||||||
const tabStrip = searchTabs ? (
|
const tabStrip = searchTabs ? (
|
||||||
<SearchTabStrip
|
<SearchTabStrip
|
||||||
|
projectId={projectId}
|
||||||
activeTabId={searchTabs.activeTabId}
|
activeTabId={searchTabs.activeTabId}
|
||||||
tabs={searchTabs.tabs}
|
tabs={searchTabs.tabs}
|
||||||
onSelect={searchTabs.onSelect}
|
onSelect={searchTabs.onSelect}
|
||||||
onClose={searchTabs.onClose}
|
onClose={searchTabs.onClose}
|
||||||
|
onViewed={searchTabs.onViewed}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
|||||||
@ -50,14 +50,12 @@ export function BacklinksSearchCard({
|
|||||||
canOpenSearch,
|
canOpenSearch,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
initialValues,
|
initialValues,
|
||||||
isFetching,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
tabLimit,
|
tabLimit,
|
||||||
}: {
|
}: {
|
||||||
canOpenSearch?: (values: SearchDraft) => boolean;
|
canOpenSearch?: (values: SearchDraft) => boolean;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
initialValues: SearchDraft;
|
initialValues: SearchDraft;
|
||||||
isFetching: boolean;
|
|
||||||
onSubmit: (values: SearchDraft) => void;
|
onSubmit: (values: SearchDraft) => void;
|
||||||
tabLimit?: number;
|
tabLimit?: number;
|
||||||
}) {
|
}) {
|
||||||
@ -145,9 +143,9 @@ export function BacklinksSearchCard({
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary lg:col-span-2"
|
className="btn btn-primary lg:col-span-2"
|
||||||
disabled={isFetching || isSubmitting}
|
disabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isFetching || isSubmitting ? "Loading..." : "Search"}
|
{isSubmitting ? "Loading..." : "Search"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</form.Subscribe>
|
</form.Subscribe>
|
||||||
|
|||||||
@ -522,10 +522,12 @@ export function DomainOverviewPage({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<SearchTabStrip
|
<SearchTabStrip
|
||||||
|
projectId={projectId}
|
||||||
activeTabId={searchTabs.activeTabId}
|
activeTabId={searchTabs.activeTabId}
|
||||||
tabs={searchTabs.tabs}
|
tabs={searchTabs.tabs}
|
||||||
onSelect={searchTabs.selectTab}
|
onSelect={searchTabs.selectTab}
|
||||||
onClose={searchTabs.closeTab}
|
onClose={searchTabs.closeTab}
|
||||||
|
onViewed={searchTabs.markTabViewed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|||||||
@ -1,15 +1,8 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { AlertCircle, ArrowLeft } from "lucide-react";
|
import { AlertCircle, ArrowLeft } from "lucide-react";
|
||||||
import { getErrorCode } from "@/client/lib/error-messages";
|
import { getErrorCode } from "@/client/lib/error-messages";
|
||||||
import { BILLING_ROUTE } from "@/shared/billing";
|
import { BILLING_ROUTE } from "@/shared/billing";
|
||||||
import {
|
|
||||||
KEYWORD_RESEARCH_STALE_TIME_MS,
|
|
||||||
buildKeywordResearchQueryKey,
|
|
||||||
buildKeywordResearchRequest,
|
|
||||||
keywordResearchQueryFn,
|
|
||||||
} from "@/client/features/keywords/hooks/useKeywordResearchData";
|
|
||||||
import { useKeywordResearchController } from "@/client/features/keywords/state/useKeywordResearchController";
|
import { useKeywordResearchController } from "@/client/features/keywords/state/useKeywordResearchController";
|
||||||
import type { KeywordResearchControllerInput } from "@/client/features/keywords/state/useKeywordResearchController";
|
import type { KeywordResearchControllerInput } from "@/client/features/keywords/state/useKeywordResearchController";
|
||||||
import type { KeywordControlsValues } from "@/client/features/keywords/hooks/useKeywordControlsForm";
|
import type { KeywordControlsValues } from "@/client/features/keywords/hooks/useKeywordControlsForm";
|
||||||
@ -20,12 +13,12 @@ import type {
|
|||||||
KeywordSearchTabInput,
|
KeywordSearchTabInput,
|
||||||
SearchTab,
|
SearchTab,
|
||||||
} from "@/client/features/search-tabs/types";
|
} from "@/client/features/search-tabs/types";
|
||||||
|
import { SearchTabStrip } from "@/client/features/search-tabs/SearchTabStrip";
|
||||||
import { useSearchTabNavigation } from "@/client/features/search-tabs/useSearchTabNavigation";
|
import { useSearchTabNavigation } from "@/client/features/search-tabs/useSearchTabNavigation";
|
||||||
import { KeywordResearchEmptyState } from "./KeywordResearchEmptyState";
|
import { KeywordResearchEmptyState } from "./KeywordResearchEmptyState";
|
||||||
import { KeywordResearchLoadingState } from "./KeywordResearchLoadingState";
|
import { KeywordResearchLoadingState } from "./KeywordResearchLoadingState";
|
||||||
import { KeywordResearchResults } from "./KeywordResearchResults";
|
import { KeywordResearchResults } from "./KeywordResearchResults";
|
||||||
import { KeywordResearchSearchBar } from "./KeywordResearchSearchBar";
|
import { KeywordResearchSearchBar } from "./KeywordResearchSearchBar";
|
||||||
import { KeywordResearchTabStrip } from "./KeywordResearchTabStrip";
|
|
||||||
import type { KeywordResearchControllerState } from "./types";
|
import type { KeywordResearchControllerState } from "./types";
|
||||||
|
|
||||||
type Props = Omit<KeywordResearchControllerInput, "onFormSubmit">;
|
type Props = Omit<KeywordResearchControllerInput, "onFormSubmit">;
|
||||||
@ -188,50 +181,6 @@ export function KeywordResearchPage(input: Props) {
|
|||||||
}));
|
}));
|
||||||
}, [controller.controlsForm, searchTabs.tabs]);
|
}, [controller.controlsForm, searchTabs.tabs]);
|
||||||
|
|
||||||
// Mark the active tab as viewed once its data lands. Reads cache state via
|
|
||||||
// the same query key the controller uses, so this catches both fresh fetches
|
|
||||||
// and warm-cache loads on tab switch.
|
|
||||||
const activeRequest = useMemo(
|
|
||||||
() =>
|
|
||||||
activeTab
|
|
||||||
? buildKeywordResearchRequest({
|
|
||||||
projectId,
|
|
||||||
keywordInput: activeTab.input.keyword,
|
|
||||||
locationCode: activeTab.input.locationCode,
|
|
||||||
resultLimit: activeTab.input.resultLimit,
|
|
||||||
mode: activeTab.input.mode,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
[activeTab, projectId],
|
|
||||||
);
|
|
||||||
const activeTabQuery = useQuery({
|
|
||||||
queryKey: buildKeywordResearchQueryKey(activeRequest),
|
|
||||||
queryFn: () => {
|
|
||||||
if (!activeRequest) throw new Error("Active tab missing request");
|
|
||||||
return keywordResearchQueryFn(activeRequest);
|
|
||||||
},
|
|
||||||
enabled: false,
|
|
||||||
staleTime: KEYWORD_RESEARCH_STALE_TIME_MS,
|
|
||||||
gcTime: KEYWORD_RESEARCH_STALE_TIME_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
const markTabViewed = searchTabs.markTabViewed;
|
|
||||||
useEffect(() => {
|
|
||||||
if (!activeTab) return;
|
|
||||||
if (!activeTabQuery.isSuccess) return;
|
|
||||||
const dataUpdatedAt = activeTabQuery.dataUpdatedAt;
|
|
||||||
if (dataUpdatedAt <= 0) return;
|
|
||||||
if (activeTab.viewedAt !== null && activeTab.viewedAt >= dataUpdatedAt) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
markTabViewed(activeTab.id, dataUpdatedAt);
|
|
||||||
}, [
|
|
||||||
activeTab,
|
|
||||||
activeTabQuery.dataUpdatedAt,
|
|
||||||
activeTabQuery.isSuccess,
|
|
||||||
markTabViewed,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-4 md:px-6 md:py-6 pb-24 md:pb-8 overflow-auto">
|
<div className="px-4 py-4 md:px-6 md:py-6 pb-24 md:pb-8 overflow-auto">
|
||||||
<div className="mx-auto flex max-w-7xl flex-col gap-5">
|
<div className="mx-auto flex max-w-7xl flex-col gap-5">
|
||||||
@ -254,12 +203,13 @@ export function KeywordResearchPage(input: Props) {
|
|||||||
<ArrowLeft className="size-4" />
|
<ArrowLeft className="size-4" />
|
||||||
Recent searches
|
Recent searches
|
||||||
</button>
|
</button>
|
||||||
<KeywordResearchTabStrip
|
<SearchTabStrip
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
tabs={searchTabs.tabs}
|
tabs={searchTabs.tabs}
|
||||||
activeTabId={searchTabs.activeTabId}
|
activeTabId={searchTabs.activeTabId}
|
||||||
onSelect={searchTabs.selectTab}
|
onSelect={searchTabs.selectTab}
|
||||||
onClose={searchTabs.closeTab}
|
onClose={searchTabs.closeTab}
|
||||||
|
onViewed={searchTabs.markTabViewed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { memo } from "react";
|
|
||||||
import { SearchTabStrip } from "@/client/features/search-tabs/SearchTabStrip";
|
|
||||||
import type { SearchTab } from "@/client/features/search-tabs/types";
|
|
||||||
import {
|
|
||||||
KEYWORD_RESEARCH_STALE_TIME_MS,
|
|
||||||
buildKeywordResearchQueryKey,
|
|
||||||
buildKeywordResearchRequest,
|
|
||||||
keywordResearchQueryFn,
|
|
||||||
} from "@/client/features/keywords/hooks/useKeywordResearchData";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
projectId: string;
|
|
||||||
tabs: SearchTab[];
|
|
||||||
activeTabId: string | null;
|
|
||||||
onSelect: (tab: SearchTab) => void;
|
|
||||||
onClose: (tabId: string) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function KeywordResearchTabStrip({
|
|
||||||
projectId,
|
|
||||||
tabs,
|
|
||||||
activeTabId,
|
|
||||||
onSelect,
|
|
||||||
onClose,
|
|
||||||
}: Props) {
|
|
||||||
if (tabs.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SearchTabStrip
|
|
||||||
activeTabId={activeTabId}
|
|
||||||
tabs={tabs}
|
|
||||||
onSelect={onSelect}
|
|
||||||
onClose={onClose}
|
|
||||||
renderLeading={(tab, active) => (
|
|
||||||
<KeywordTabStatus tab={tab} projectId={projectId} active={active} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeywordTabStatus = memo(function KeywordTabStatus({
|
|
||||||
tab,
|
|
||||||
projectId,
|
|
||||||
active,
|
|
||||||
}: {
|
|
||||||
tab: SearchTab;
|
|
||||||
projectId: string;
|
|
||||||
active: boolean;
|
|
||||||
}) {
|
|
||||||
if (tab.input.type !== "keyword") return null;
|
|
||||||
|
|
||||||
const request = buildKeywordResearchRequest({
|
|
||||||
projectId,
|
|
||||||
keywordInput: tab.input.keyword,
|
|
||||||
locationCode: tab.input.locationCode,
|
|
||||||
resultLimit: tab.input.resultLimit,
|
|
||||||
mode: tab.input.mode,
|
|
||||||
});
|
|
||||||
const queryKey = buildKeywordResearchQueryKey(request);
|
|
||||||
|
|
||||||
// enabled: false — observer only. The active tab's controller owns fetching.
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey,
|
|
||||||
queryFn: () => {
|
|
||||||
if (!request) throw new Error("Tab is missing a research request");
|
|
||||||
return keywordResearchQueryFn(request);
|
|
||||||
},
|
|
||||||
enabled: false,
|
|
||||||
select: () => null,
|
|
||||||
notifyOnChangeProps: ["dataUpdatedAt", "errorUpdatedAt"],
|
|
||||||
staleTime: KEYWORD_RESEARCH_STALE_TIME_MS,
|
|
||||||
gcTime: KEYWORD_RESEARCH_STALE_TIME_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasResult = query.dataUpdatedAt > 0;
|
|
||||||
const unviewed =
|
|
||||||
!active &&
|
|
||||||
hasResult &&
|
|
||||||
(tab.viewedAt === null || tab.viewedAt < query.dataUpdatedAt);
|
|
||||||
const isError = query.isError;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className="flex w-3.5 shrink-0 items-center justify-center"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
{isError ? (
|
|
||||||
<span className="size-2 rounded-full bg-error" />
|
|
||||||
) : unviewed ? (
|
|
||||||
<span className="size-2 rounded-full bg-primary" />
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@ -1,22 +1,42 @@
|
|||||||
import { X } from "lucide-react";
|
import { useEffect } from "react";
|
||||||
import type { ReactNode } 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 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";
|
export type { SearchTab } from "./types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeTabId: string | null;
|
activeTabId: string | null;
|
||||||
|
projectId: string;
|
||||||
tabs: SearchTab[];
|
tabs: SearchTab[];
|
||||||
onSelect: (tab: SearchTab) => void;
|
onSelect: (tab: SearchTab) => void;
|
||||||
onClose: (tabId: string) => void;
|
onClose: (tabId: string) => void;
|
||||||
renderLeading?: (tab: SearchTab, active: boolean) => ReactNode;
|
onViewed: (tabId: string, when?: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SearchTabQueryConfig = {
|
||||||
|
queryKey: QueryKey;
|
||||||
|
queryFn: () => Promise<unknown>;
|
||||||
|
staleTime?: number;
|
||||||
|
gcTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SearchTabStrip({
|
export function SearchTabStrip({
|
||||||
activeTabId,
|
activeTabId,
|
||||||
|
projectId,
|
||||||
tabs,
|
tabs,
|
||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
onClose,
|
||||||
renderLeading,
|
onViewed,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
if (tabs.length === 0) return null;
|
if (tabs.length === 0) return null;
|
||||||
|
|
||||||
@ -46,7 +66,12 @@ export function SearchTabStrip({
|
|||||||
className="flex min-w-0 items-center gap-1.5 px-2.5 py-1.5 text-left"
|
className="flex min-w-0 items-center gap-1.5 px-2.5 py-1.5 text-left"
|
||||||
onClick={() => onSelect(tab)}
|
onClick={() => onSelect(tab)}
|
||||||
>
|
>
|
||||||
{renderLeading ? renderLeading(tab, active) : null}
|
<SearchTabStatus
|
||||||
|
tab={tab}
|
||||||
|
projectId={projectId}
|
||||||
|
active={active}
|
||||||
|
onViewed={onViewed}
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
className="max-w-[10rem] truncate font-medium"
|
className="max-w-[10rem] truncate font-medium"
|
||||||
title={tab.label}
|
title={tab.label}
|
||||||
@ -70,3 +95,140 @@ export function SearchTabStrip({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 <SearchTabStatusIndicator status={status} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SearchTabStatusIndicator({
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
status: "idle" | "loading" | "unviewed" | "error";
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="flex w-3.5 shrink-0 items-center justify-center"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{status === "loading" ? (
|
||||||
|
<Loader2 className="size-3 animate-spin text-base-content/50" />
|
||||||
|
) : status === "error" ? (
|
||||||
|
<span className="size-2 rounded-full bg-error" />
|
||||||
|
) : status === "unviewed" ? (
|
||||||
|
<span className="size-2 rounded-full bg-primary" />
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user