fix: use Tanstack Query for Keyword Research (#150)
* keywords: use query cache for research requests * keywords: simplify research request dedupe
This commit is contained in:
parent
2fc568f73c
commit
db1a1d723f
@ -1,4 +1,5 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useEffect, useMemo, useRef } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { getStandardErrorMessage } from "@/client/lib/error-messages";
|
import { getStandardErrorMessage } from "@/client/lib/error-messages";
|
||||||
import { captureClientEvent } from "@/client/lib/posthog";
|
import { captureClientEvent } from "@/client/lib/posthog";
|
||||||
import { LOCATIONS, getLanguageCode } from "@/client/features/keywords/utils";
|
import { LOCATIONS, getLanguageCode } from "@/client/features/keywords/utils";
|
||||||
@ -9,7 +10,6 @@ import type {
|
|||||||
KeywordSource,
|
KeywordSource,
|
||||||
ResultLimit,
|
ResultLimit,
|
||||||
} from "@/client/features/keywords/keywordResearchTypes";
|
} from "@/client/features/keywords/keywordResearchTypes";
|
||||||
import type { KeywordResearchRow } from "@/types/keywords";
|
|
||||||
|
|
||||||
type AddSearchFn = (
|
type AddSearchFn = (
|
||||||
keyword: string,
|
keyword: string,
|
||||||
@ -17,130 +17,153 @@ type AddSearchFn = (
|
|||||||
locationName: string,
|
locationName: string,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
export function useKeywordResearchData(addSearch: AddSearchFn) {
|
type KeywordResearchQueryInput = {
|
||||||
const [rows, setRows] = useState<KeywordResearchRow[]>([]);
|
projectId: string;
|
||||||
const [hasSearched, setHasSearched] = useState(false);
|
keywordInput: string;
|
||||||
const [lastSearchError, setLastSearchError] = useState(false);
|
locationCode: number;
|
||||||
const [lastResultSource, setLastResultSource] =
|
resultLimit: ResultLimit;
|
||||||
useState<KeywordSource>("related");
|
mode: KeywordMode;
|
||||||
const [lastUsedFallback, setLastUsedFallback] = useState(false);
|
};
|
||||||
const [lastSearchKeyword, setLastSearchKeyword] = useState("");
|
|
||||||
const [lastSearchLocationCode, setLastSearchLocationCode] = useState(
|
type KeywordResearchRequest = {
|
||||||
DEFAULT_LOCATION_CODE,
|
projectId: string;
|
||||||
|
keywords: string[];
|
||||||
|
seedKeyword: string;
|
||||||
|
locationCode: number;
|
||||||
|
languageCode: string;
|
||||||
|
resultLimit: ResultLimit;
|
||||||
|
mode: KeywordMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEYWORD_RESEARCH_STALE_TIME_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function parseSearchKeywords(value: string) {
|
||||||
|
return value
|
||||||
|
.split(/[\n,]/)
|
||||||
|
.map((keyword) => keyword.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildKeywordResearchQueryKey(request: KeywordResearchRequest | null) {
|
||||||
|
return request
|
||||||
|
? [
|
||||||
|
"keywordResearch",
|
||||||
|
request.projectId,
|
||||||
|
request.keywords,
|
||||||
|
request.locationCode,
|
||||||
|
request.languageCode,
|
||||||
|
request.resultLimit,
|
||||||
|
request.mode,
|
||||||
|
]
|
||||||
|
: ["keywordResearch", "idle"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useKeywordResearchData(
|
||||||
|
input: KeywordResearchQueryInput,
|
||||||
|
addSearch: AddSearchFn,
|
||||||
|
) {
|
||||||
|
const keywords = useMemo(
|
||||||
|
() => parseSearchKeywords(input.keywordInput),
|
||||||
|
[input.keywordInput],
|
||||||
);
|
);
|
||||||
const [researchError, setResearchError] = useState<string | null>(null);
|
const request = useMemo<KeywordResearchRequest | null>(() => {
|
||||||
const [researchMutationError, setResearchMutationError] =
|
const seedKeyword = keywords[0] ?? "";
|
||||||
useState<unknown>(null);
|
if (!seedKeyword) return null;
|
||||||
const [searchedKeyword, setSearchedKeyword] = useState("");
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
// Sequence token so a stale fetch (e.g. user fired a second search before
|
|
||||||
// the first resolved) can't overwrite state that belongs to a newer one.
|
|
||||||
const requestSeqRef = useRef(0);
|
|
||||||
|
|
||||||
const beginSearch = (seedKeyword: string, locationCode: number) => {
|
return {
|
||||||
setResearchError(null);
|
projectId: input.projectId,
|
||||||
setResearchMutationError(null);
|
keywords,
|
||||||
setHasSearched(true);
|
seedKeyword,
|
||||||
setLastSearchError(false);
|
locationCode: input.locationCode,
|
||||||
setSearchedKeyword(seedKeyword);
|
languageCode: getLanguageCode(input.locationCode),
|
||||||
setLastSearchKeyword(seedKeyword);
|
resultLimit: input.resultLimit,
|
||||||
setLastSearchLocationCode(locationCode);
|
mode: input.mode,
|
||||||
setIsLoading(true);
|
};
|
||||||
};
|
}, [
|
||||||
|
input.locationCode,
|
||||||
|
input.mode,
|
||||||
|
input.projectId,
|
||||||
|
input.resultLimit,
|
||||||
|
keywords,
|
||||||
|
]);
|
||||||
|
const queryKey = useMemo(
|
||||||
|
() => buildKeywordResearchQueryKey(request),
|
||||||
|
[request],
|
||||||
|
);
|
||||||
|
const queryKeyString = JSON.stringify(queryKey);
|
||||||
|
|
||||||
const resetResearch = () => {
|
const researchQuery = useQuery({
|
||||||
setRows([]);
|
queryKey,
|
||||||
setHasSearched(false);
|
queryFn: () => {
|
||||||
setLastSearchError(false);
|
if (!request) {
|
||||||
setLastResultSource("related");
|
throw new Error("Keyword research query ran without request params");
|
||||||
setLastUsedFallback(false);
|
|
||||||
setLastSearchKeyword("");
|
|
||||||
setLastSearchLocationCode(DEFAULT_LOCATION_CODE);
|
|
||||||
setResearchError(null);
|
|
||||||
setResearchMutationError(null);
|
|
||||||
setSearchedKeyword("");
|
|
||||||
setIsLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const runSearch = async (
|
|
||||||
input: {
|
|
||||||
projectId: string;
|
|
||||||
keywords: string[];
|
|
||||||
locationCode: number;
|
|
||||||
resultLimit: ResultLimit;
|
|
||||||
mode: KeywordMode;
|
|
||||||
},
|
|
||||||
handlers?: {
|
|
||||||
onSuccess?: (seedKeyword: string, rows: KeywordResearchRow[]) => void;
|
|
||||||
onError?: () => void;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
const seedKeyword = input.keywords[0] ?? "";
|
|
||||||
const languageCode = getLanguageCode(input.locationCode);
|
|
||||||
const requestSeq = ++requestSeqRef.current;
|
|
||||||
const isStale = () => requestSeqRef.current !== requestSeq;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await researchKeywords({
|
|
||||||
data: {
|
|
||||||
keywords: input.keywords,
|
|
||||||
projectId: input.projectId,
|
|
||||||
locationCode: input.locationCode,
|
|
||||||
languageCode,
|
|
||||||
resultLimit: input.resultLimit,
|
|
||||||
mode: input.mode,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isStale()) return;
|
|
||||||
|
|
||||||
setResearchError(null);
|
|
||||||
setResearchMutationError(null);
|
|
||||||
setRows(result.rows);
|
|
||||||
setLastResultSource(result.source);
|
|
||||||
setLastUsedFallback(result.usedFallback);
|
|
||||||
|
|
||||||
captureClientEvent("keyword_research:search_complete", {
|
|
||||||
location_code: input.locationCode,
|
|
||||||
search_mode: input.mode,
|
|
||||||
result_count: result.rows.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (seedKeyword) {
|
|
||||||
addSearch(
|
|
||||||
seedKeyword,
|
|
||||||
input.locationCode,
|
|
||||||
LOCATIONS[input.locationCode] || "Unknown",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlers?.onSuccess?.(seedKeyword, result.rows);
|
return researchKeywords({
|
||||||
} catch (error) {
|
data: {
|
||||||
if (isStale()) return;
|
projectId: request.projectId,
|
||||||
setLastSearchError(true);
|
keywords: request.keywords,
|
||||||
setRows([]);
|
locationCode: request.locationCode,
|
||||||
setResearchMutationError(error);
|
languageCode: request.languageCode,
|
||||||
setResearchError(getStandardErrorMessage(error, "Research failed."));
|
resultLimit: request.resultLimit,
|
||||||
handlers?.onError?.();
|
mode: request.mode,
|
||||||
} finally {
|
},
|
||||||
if (!isStale()) setIsLoading(false);
|
});
|
||||||
}
|
},
|
||||||
};
|
enabled: request !== null,
|
||||||
|
staleTime: KEYWORD_RESEARCH_STALE_TIME_MS,
|
||||||
|
gcTime: KEYWORD_RESEARCH_STALE_TIME_MS,
|
||||||
|
retry: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handledSuccessKeyRef = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!request || !researchQuery.isSuccess || !researchQuery.data) return;
|
||||||
|
if (handledSuccessKeyRef.current === queryKeyString) return;
|
||||||
|
handledSuccessKeyRef.current = queryKeyString;
|
||||||
|
|
||||||
|
captureClientEvent("keyword_research:search_complete", {
|
||||||
|
location_code: request.locationCode,
|
||||||
|
search_mode: request.mode,
|
||||||
|
result_count: researchQuery.data.rows.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
addSearch(
|
||||||
|
request.seedKeyword,
|
||||||
|
request.locationCode,
|
||||||
|
LOCATIONS[request.locationCode] || "Unknown",
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
addSearch,
|
||||||
|
queryKeyString,
|
||||||
|
request,
|
||||||
|
researchQuery.data,
|
||||||
|
researchQuery.isSuccess,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hasSearched = request !== null;
|
||||||
|
const rows = hasSearched ? (researchQuery.data?.rows ?? []) : [];
|
||||||
|
const researchError =
|
||||||
|
hasSearched && researchQuery.isError
|
||||||
|
? getStandardErrorMessage(researchQuery.error, "Research failed.")
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rows,
|
rows,
|
||||||
hasSearched,
|
hasSearched,
|
||||||
lastSearchError,
|
lastSearchError: hasSearched && researchQuery.isError,
|
||||||
lastResultSource,
|
lastResultSource:
|
||||||
lastUsedFallback,
|
researchQuery.data?.source ?? ("related" as KeywordSource),
|
||||||
lastSearchKeyword,
|
lastUsedFallback: researchQuery.data?.usedFallback ?? false,
|
||||||
lastSearchLocationCode,
|
lastSearchKeyword: request?.seedKeyword ?? "",
|
||||||
|
lastSearchLocationCode: request?.locationCode ?? DEFAULT_LOCATION_CODE,
|
||||||
researchError,
|
researchError,
|
||||||
researchMutationError,
|
researchMutationError: researchQuery.error,
|
||||||
searchedKeyword,
|
searchedKeyword: request?.seedKeyword ?? "",
|
||||||
isLoading,
|
isLoading: hasSearched && researchQuery.isPending,
|
||||||
beginSearch,
|
researchQuery,
|
||||||
resetResearch,
|
retryResearch: researchQuery.refetch,
|
||||||
runSearch,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,10 +81,7 @@ function KeywordResearchContent({
|
|||||||
Go to Billing
|
Go to Billing
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button className="btn btn-sm" onClick={controller.retrySearch}>
|
||||||
className="btn btn-sm"
|
|
||||||
onClick={() => controller.onSearch()}
|
|
||||||
>
|
|
||||||
Try again
|
Try again
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import type { SortDir, SortField } from "@/client/features/keywords/components";
|
|||||||
import {
|
import {
|
||||||
buildKeywordSearchKey,
|
buildKeywordSearchKey,
|
||||||
getNextSortParams,
|
getNextSortParams,
|
||||||
parseKeywordInput,
|
|
||||||
useSaveAndExportActions,
|
useSaveAndExportActions,
|
||||||
} from "./keywordControllerActions";
|
} from "./keywordControllerActions";
|
||||||
import {
|
import {
|
||||||
@ -45,6 +44,7 @@ export function useKeywordResearchController(
|
|||||||
const state = useKeywordControllerState(input);
|
const state = useKeywordControllerState(input);
|
||||||
const controlsForm = state.controlsForm;
|
const controlsForm = state.controlsForm;
|
||||||
const setSearchParams = state.setSearchParams;
|
const setSearchParams = state.setSearchParams;
|
||||||
|
const retryResearch = state.retryResearch;
|
||||||
|
|
||||||
const onSearch = useCallback(
|
const onSearch = useCallback(
|
||||||
(overrides?: Partial<{ keyword: string; locationCode: number }>) => {
|
(overrides?: Partial<{ keyword: string; locationCode: number }>) => {
|
||||||
@ -61,6 +61,10 @@ export function useKeywordResearchController(
|
|||||||
[controlsForm],
|
[controlsForm],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const retrySearch = useCallback(() => {
|
||||||
|
void retryResearch();
|
||||||
|
}, [retryResearch]);
|
||||||
|
|
||||||
const handleSearchSubmit = useCallback(
|
const handleSearchSubmit = useCallback(
|
||||||
(event: FormEvent) => {
|
(event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -124,6 +128,7 @@ export function useKeywordResearchController(
|
|||||||
removeHistoryItem: state.removeHistoryItem,
|
removeHistoryItem: state.removeHistoryItem,
|
||||||
researchError: state.researchError,
|
researchError: state.researchError,
|
||||||
researchMutationError: state.researchMutationError,
|
researchMutationError: state.researchMutationError,
|
||||||
|
retrySearch,
|
||||||
resetFilters: state.resetFilters,
|
resetFilters: state.resetFilters,
|
||||||
rows: state.rows,
|
rows: state.rows,
|
||||||
searchedKeyword: state.searchedKeyword,
|
searchedKeyword: state.searchedKeyword,
|
||||||
@ -191,66 +196,41 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) {
|
|||||||
lastSearchLocationCode,
|
lastSearchLocationCode,
|
||||||
researchError,
|
researchError,
|
||||||
researchMutationError,
|
researchMutationError,
|
||||||
|
researchQuery,
|
||||||
searchedKeyword,
|
searchedKeyword,
|
||||||
isLoading,
|
isLoading,
|
||||||
beginSearch,
|
retryResearch,
|
||||||
resetResearch,
|
} = useKeywordResearchData(
|
||||||
runSearch,
|
{
|
||||||
} = useKeywordResearchData(addSearch);
|
projectId: input.projectId,
|
||||||
|
keywordInput: input.keywordInput,
|
||||||
|
locationCode,
|
||||||
|
resultLimit: input.resultLimit,
|
||||||
|
mode: input.keywordMode,
|
||||||
|
},
|
||||||
|
addSearch,
|
||||||
|
);
|
||||||
const setSearchParams = useKeywordSearchParams();
|
const setSearchParams = useKeywordSearchParams();
|
||||||
const saveMutation = useKeywordSaveMutation(input.projectId);
|
const saveMutation = useKeywordSaveMutation(input.projectId);
|
||||||
|
|
||||||
// Tracks the parameters used for the most recent search trigger so the
|
const activeSearchKey = input.keywordInput.trim()
|
||||||
// URL-driven effect below doesn't re-fire after the form-submit path
|
? buildKeywordSearchKey({
|
||||||
// already kicked off a search for the same params.
|
keyword: input.keywordInput,
|
||||||
const lastTriggerKeyRef = useRef<string | null>(null);
|
locationCode,
|
||||||
|
resultLimit: input.resultLimit,
|
||||||
|
mode: input.keywordMode,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const triggerSearch = useCallback(
|
const previousSearchKeyRef = useRef<string | null>(null);
|
||||||
(params: {
|
const handledSerpSearchKeyRef = useRef<string | null>(null);
|
||||||
keyword: string;
|
|
||||||
locationCode: number;
|
|
||||||
resultLimit: ResultLimit;
|
|
||||||
mode: KeywordMode;
|
|
||||||
}) => {
|
|
||||||
const keywords = parseKeywordInput(params.keyword);
|
|
||||||
if (keywords.length === 0) return;
|
|
||||||
|
|
||||||
lastTriggerKeyRef.current = buildKeywordSearchKey(params);
|
const clearActiveKeywordResult = useCallback(() => {
|
||||||
uiState.setSelectedKeyword(null);
|
clearSelection();
|
||||||
clearSelection();
|
uiState.setSelectedKeyword(null);
|
||||||
setSerpKeyword(null);
|
setSerpKeyword(null);
|
||||||
beginSearch(keywords[0] ?? "", params.locationCode);
|
setSerpPage(0);
|
||||||
|
}, [clearSelection, setSerpKeyword, setSerpPage, uiState]);
|
||||||
void runSearch(
|
|
||||||
{
|
|
||||||
projectId: input.projectId,
|
|
||||||
keywords,
|
|
||||||
locationCode: params.locationCode,
|
|
||||||
resultLimit: params.resultLimit,
|
|
||||||
mode: params.mode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: (seedKeyword, nextRows) => {
|
|
||||||
if (nextRows.length === 0) {
|
|
||||||
setSerpKeyword(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSerpKeyword(seedKeyword);
|
|
||||||
setSerpPage(0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[
|
|
||||||
beginSearch,
|
|
||||||
clearSelection,
|
|
||||||
input.projectId,
|
|
||||||
runSearch,
|
|
||||||
setSerpKeyword,
|
|
||||||
setSerpPage,
|
|
||||||
uiState,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
const controlsForm = useKeywordControlsForm(
|
const controlsForm = useKeywordControlsForm(
|
||||||
{
|
{
|
||||||
@ -269,63 +249,39 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) {
|
|||||||
kLimit: value.resultLimit === 150 ? undefined : value.resultLimit,
|
kLimit: value.resultLimit === 150 ? undefined : value.resultLimit,
|
||||||
mode: value.mode === "auto" ? undefined : value.mode,
|
mode: value.mode === "auto" ? undefined : value.mode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Trigger immediately so re-submitting the same query (URL unchanged)
|
|
||||||
// still refetches. The dedup ref prevents the URL effect below from
|
|
||||||
// double-firing in the typical (URL-changes) case.
|
|
||||||
triggerSearch({
|
|
||||||
keyword: value.keyword,
|
|
||||||
locationCode: value.locationCode,
|
|
||||||
resultLimit: value.resultLimit,
|
|
||||||
mode: value.mode,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// URL-driven search trigger. Fires when the user lands on a shareable URL
|
// The URL is the source of truth for paid keyword research queries. This
|
||||||
// (direct link, cmd+click on a history item, browser back/forward) so the
|
// effect only resets UI state around a new query key; TanStack Query owns the
|
||||||
// page reproduces the search those params describe without a form submit.
|
// actual fetch, cache, dedupe, and error lifecycle.
|
||||||
// When the URL is cleared (no `q`), the page resets to the recent-searches
|
|
||||||
// empty state so the "Recent searches" Link works without an extra handler.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const trimmed = input.keywordInput.trim();
|
if (activeSearchKey === previousSearchKeyRef.current) return;
|
||||||
|
previousSearchKeyRef.current = activeSearchKey;
|
||||||
|
handledSerpSearchKeyRef.current = null;
|
||||||
|
|
||||||
if (trimmed.length === 0) {
|
if (!activeSearchKey) {
|
||||||
if (lastTriggerKeyRef.current === null) return;
|
clearActiveKeywordResult();
|
||||||
lastTriggerKeyRef.current = null;
|
|
||||||
resetResearch();
|
|
||||||
clearSelection();
|
|
||||||
uiState.setSelectedKeyword(null);
|
|
||||||
setSerpKeyword(null);
|
|
||||||
setSerpPage(0);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlKey = buildKeywordSearchKey({
|
clearActiveKeywordResult();
|
||||||
keyword: input.keywordInput,
|
}, [activeSearchKey, clearActiveKeywordResult]);
|
||||||
locationCode,
|
|
||||||
resultLimit: input.resultLimit,
|
|
||||||
mode: input.keywordMode,
|
|
||||||
});
|
|
||||||
if (urlKey === lastTriggerKeyRef.current) return;
|
|
||||||
|
|
||||||
triggerSearch({
|
useEffect(() => {
|
||||||
keyword: input.keywordInput,
|
if (!activeSearchKey || !researchQuery.isSuccess) return;
|
||||||
locationCode,
|
if (handledSerpSearchKeyRef.current === activeSearchKey) return;
|
||||||
resultLimit: input.resultLimit,
|
|
||||||
mode: input.keywordMode,
|
handledSerpSearchKeyRef.current = activeSearchKey;
|
||||||
});
|
setSerpKeyword(rows.length > 0 ? searchedKeyword : null);
|
||||||
|
setSerpPage(0);
|
||||||
}, [
|
}, [
|
||||||
clearSelection,
|
activeSearchKey,
|
||||||
input.keywordInput,
|
researchQuery.isSuccess,
|
||||||
input.keywordMode,
|
rows.length,
|
||||||
input.resultLimit,
|
searchedKeyword,
|
||||||
locationCode,
|
|
||||||
resetResearch,
|
|
||||||
setSerpKeyword,
|
setSerpKeyword,
|
||||||
setSerpPage,
|
setSerpPage,
|
||||||
triggerSearch,
|
|
||||||
uiState,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { filteredRows, activeFilterCount } = useKeywordFiltering({
|
const { filteredRows, activeFilterCount } = useKeywordFiltering({
|
||||||
@ -349,7 +305,6 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) {
|
|||||||
return {
|
return {
|
||||||
activeFilterCount,
|
activeFilterCount,
|
||||||
activeSerpKeyword,
|
activeSerpKeyword,
|
||||||
beginSearch,
|
|
||||||
clearSelection,
|
clearSelection,
|
||||||
controlsForm,
|
controlsForm,
|
||||||
filteredRows,
|
filteredRows,
|
||||||
@ -366,10 +321,9 @@ function useKeywordControllerState(input: KeywordResearchControllerInput) {
|
|||||||
mobileTab: uiState.mobileTab,
|
mobileTab: uiState.mobileTab,
|
||||||
overviewKeyword,
|
overviewKeyword,
|
||||||
removeHistoryItem,
|
removeHistoryItem,
|
||||||
resetResearch,
|
|
||||||
researchError,
|
researchError,
|
||||||
researchMutationError,
|
researchMutationError,
|
||||||
runSearch,
|
retryResearch,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
rows,
|
rows,
|
||||||
searchedKeyword,
|
searchedKeyword,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link, useLocation } from "@tanstack/react-router";
|
import { Link, useLocation } from "@tanstack/react-router";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
@ -35,47 +36,40 @@ export function AuthenticatedAppLayout({
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
||||||
const setupModalRef = React.useRef<HTMLDivElement | null>(null);
|
const setupModalRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const [isSeoApiKeyConfigured, setIsSeoApiKeyConfigured] = React.useState<
|
|
||||||
boolean | null
|
|
||||||
>(null);
|
|
||||||
const [seoApiKeyStatusError, setSeoApiKeyStatusError] = React.useState(false);
|
|
||||||
const [showMissingSeoApiKeyModal, setShowMissingSeoApiKeyModal] =
|
const [showMissingSeoApiKeyModal, setShowMissingSeoApiKeyModal] =
|
||||||
React.useState(false);
|
React.useState(false);
|
||||||
|
const shouldCheckSeoApiKeyStatus = location.pathname !== BILLING_ROUTE;
|
||||||
|
const seoApiKeyStatusQuery = useQuery({
|
||||||
|
queryKey: ["seoApiKeyStatus"],
|
||||||
|
queryFn: () => getSeoApiKeyStatus(),
|
||||||
|
enabled: shouldCheckSeoApiKeyStatus,
|
||||||
|
});
|
||||||
|
const isSeoApiKeyConfigured = shouldCheckSeoApiKeyStatus
|
||||||
|
? (seoApiKeyStatusQuery.data?.configured ?? null)
|
||||||
|
: null;
|
||||||
|
const seoApiKeyStatusError =
|
||||||
|
shouldCheckSeoApiKeyStatus && seoApiKeyStatusQuery.isError;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (location.pathname === BILLING_ROUTE) {
|
if (!shouldCheckSeoApiKeyStatus) {
|
||||||
setSeoApiKeyStatusError(false);
|
|
||||||
setIsSeoApiKeyConfigured(null);
|
|
||||||
setShowMissingSeoApiKeyModal(false);
|
setShowMissingSeoApiKeyModal(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
if (seoApiKeyStatusQuery.isError) {
|
||||||
|
setShowMissingSeoApiKeyModal(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const checkSeoApiKeyStatus = async () => {
|
if (!seoApiKeyStatusQuery.isSuccess) return;
|
||||||
try {
|
setShowMissingSeoApiKeyModal(!seoApiKeyStatusQuery.data.configured);
|
||||||
const result = await getSeoApiKeyStatus();
|
}, [
|
||||||
if (cancelled) return;
|
location.pathname,
|
||||||
|
seoApiKeyStatusQuery.data,
|
||||||
setSeoApiKeyStatusError(false);
|
seoApiKeyStatusQuery.isError,
|
||||||
setIsSeoApiKeyConfigured(result.configured);
|
seoApiKeyStatusQuery.isSuccess,
|
||||||
if (!result.configured) {
|
shouldCheckSeoApiKeyStatus,
|
||||||
setShowMissingSeoApiKeyModal(true);
|
]);
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (cancelled) return;
|
|
||||||
setSeoApiKeyStatusError(true);
|
|
||||||
setIsSeoApiKeyConfigured(null);
|
|
||||||
setShowMissingSeoApiKeyModal(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void checkSeoApiKeyStatus();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
const shouldShowMissingSeoApiKeyModal =
|
const shouldShowMissingSeoApiKeyModal =
|
||||||
showMissingSeoApiKeyModal && location.pathname !== DATAFORSEO_HELP_PATH;
|
showMissingSeoApiKeyModal && location.pathname !== DATAFORSEO_HELP_PATH;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user