metatron-open-seo/src/client/features/keywords/state/useKeywordResearchController.ts

333 lines
9.5 KiB
TypeScript

import { useCallback, type FormEvent } from "react";
import { useKeywordControlsForm } from "@/client/features/keywords/hooks/useKeywordControlsForm";
import { useKeywordFiltering } from "@/client/features/keywords/hooks/useKeywordFiltering";
import { useLocalKeywordFilters } from "@/client/features/keywords/hooks/useLocalKeywordFilters";
import { useKeywordResearchData } from "@/client/features/keywords/hooks/useKeywordResearchData";
import { useKeywordSelection } from "@/client/features/keywords/hooks/useKeywordSelection";
import { useKeywordSerpAnalysis } from "@/client/features/keywords/hooks/useKeywordSerpAnalysis";
import { captureClientEvent } from "@/client/lib/posthog";
import { useSearchHistory } from "@/client/hooks/useSearchHistory";
import {
type KeywordMode,
type ResultLimit,
} from "@/client/features/keywords/keywordResearchTypes";
import { DEFAULT_LOCATION_CODE } from "@/client/features/keywords/locations";
import type { KeywordResearchRow } from "@/types/keywords";
import type { SortDir, SortField } from "@/client/features/keywords/components";
import {
getNextSortParams,
parseKeywordInput,
useSaveAndExportActions,
} from "./keywordControllerActions";
import {
useKeywordSaveMutation,
useKeywordSearchParams,
useKeywordUiState,
useResolvedKeywordLocation,
} from "./keywordControllerInternals";
import { useKeywordOverviewState } from "./useKeywordOverviewState";
export type KeywordResearchControllerInput = {
projectId: string;
keywordInput: string;
locationCode: number;
hasExplicitLocationCode: boolean;
resultLimit: ResultLimit;
keywordMode: KeywordMode;
sortField: SortField;
sortDir: SortDir;
};
export function useKeywordResearchController(
input: KeywordResearchControllerInput,
) {
const state = useKeywordControllerState(input);
const controlsForm = state.controlsForm;
const setSearchParams = state.setSearchParams;
const onSearch = useCallback(
(overrides?: Partial<{ keyword: string; locationCode: number }>) => {
if (overrides?.keyword !== undefined) {
controlsForm.setFieldValue("keyword", overrides.keyword);
}
if (overrides?.locationCode !== undefined) {
controlsForm.setFieldValue("locationCode", overrides.locationCode);
}
void controlsForm.handleSubmit();
},
[controlsForm],
);
const handleSearchSubmit = useCallback(
(event: FormEvent) => {
event.preventDefault();
void controlsForm.handleSubmit();
},
[controlsForm],
);
const toggleSort = useCallback(
(field: SortField) => {
setSearchParams(getNextSortParams(input.sortField, input.sortDir, field));
},
[input.sortDir, input.sortField, setSearchParams],
);
const { handleSaveKeywords, confirmSave, exportCsv } =
useSaveAndExportActions({
selectedRows: state.selectedRows,
rows: state.rows,
filteredRows: state.filteredRows,
input,
saveKeywordsMutate: state.saveMutation.mutate,
setShowSaveDialog: state.setShowSaveDialog,
});
const handleToggleAllRows = () => {
state.toggleAllRows(state.filteredRows.map((row) => row.keyword));
};
const handleRowClick = (row: KeywordResearchRow) => {
captureClientEvent("keyword_research:serp_open");
state.setSelectedKeyword(row);
state.setSerpKeyword(row.keyword);
state.setSerpPage(0);
};
const resetView = useCallback(() => {
state.resetResearch();
state.clearSelection();
state.resetFilters();
state.setSelectedKeyword(null);
state.setSerpKeyword(null);
state.setSerpPage(0);
state.setMobileTab("keywords");
state.setShowFilters(false);
state.setShowSaveDialog(false);
}, [state]);
return {
activeFilterCount: state.activeFilterCount,
activeSerpKeyword: state.activeSerpKeyword,
confirmSave,
controlsForm: state.controlsForm,
exportCsv,
filteredRows: state.filteredRows,
filtersForm: state.filtersForm,
handleRowClick,
handleSaveKeywords,
handleSearchSubmit,
hasSearched: state.hasSearched,
history: state.history,
historyLoaded: state.historyLoaded,
isLoading: state.isLoading,
lastResultSource: state.lastResultSource,
lastSearchError: state.lastSearchError,
lastSearchKeyword: state.lastSearchKeyword,
lastSearchLocationCode: state.lastSearchLocationCode,
lastUsedFallback: state.lastUsedFallback,
mobileTab: state.mobileTab,
onSearch,
overviewKeyword: state.overviewKeyword,
removeHistoryItem: state.removeHistoryItem,
researchError: state.researchError,
researchMutationError: state.researchMutationError,
resetView,
resetFilters: state.resetFilters,
rows: state.rows,
searchedKeyword: state.searchedKeyword,
selectedRows: state.selectedRows,
serpError: state.serpError,
serpLoading: state.serpLoading,
serpPage: state.serpPage,
serpQuery: state.serpQuery,
serpResults: state.serpResults,
setMobileTab: state.setMobileTab,
setSerpPage: state.setSerpPage,
setShowFilters: state.setShowFilters,
setShowSaveDialog: state.setShowSaveDialog,
showApproximateMatchNotice: state.showApproximateMatchNotice,
showFilters: state.showFilters,
showSaveDialog: state.showSaveDialog,
sortDir: input.sortDir,
sortField: input.sortField,
toggleAllRows: handleToggleAllRows,
toggleRowSelection: state.toggleRowSelection,
toggleSort,
SERP_PAGE_SIZE: state.SERP_PAGE_SIZE,
};
}
function useKeywordControllerState(input: KeywordResearchControllerInput) {
const { locationCode, setPreferredLocationCode } =
useResolvedKeywordLocation(input);
const {
filtersForm,
values: filterValues,
resetFilters,
} = useLocalKeywordFilters();
const uiState = useKeywordUiState(
Object.values(filterValues).some((v) => v.trim() !== ""),
);
const { selectedRows, clearSelection, toggleRowSelection, toggleAllRows } =
useKeywordSelection();
const {
setSerpKeyword,
serpPage,
setSerpPage,
SERP_PAGE_SIZE,
serpQuery,
serpResults,
activeSerpKeyword,
serpLoading,
serpError,
} = useKeywordSerpAnalysis(input.projectId, locationCode);
const {
history,
isLoaded: historyLoaded,
addSearch,
removeHistoryItem,
} = useSearchHistory(input.projectId);
const {
rows,
hasSearched,
lastSearchError,
lastResultSource,
lastUsedFallback,
lastSearchKeyword,
lastSearchLocationCode,
researchError,
researchMutationError,
searchedKeyword,
isLoading,
beginSearch,
resetResearch,
runSearch,
} = useKeywordResearchData(addSearch);
const setSearchParams = useKeywordSearchParams();
const saveMutation = useKeywordSaveMutation(input.projectId);
const controlsForm = useKeywordControlsForm(
{
...input,
locationCode,
},
(value) => {
const keywords = parseKeywordInput(value.keyword);
const activeLocation = value.locationCode;
const activeResultLimit = value.resultLimit;
const activeMode = value.mode;
setPreferredLocationCode(activeLocation);
setSearchParams({
q: value.keyword,
loc:
input.hasExplicitLocationCode ||
activeLocation !== DEFAULT_LOCATION_CODE
? activeLocation
: undefined,
kLimit: activeResultLimit === 150 ? undefined : activeResultLimit,
mode: activeMode === "auto" ? undefined : activeMode,
});
uiState.setSelectedKeyword(null);
clearSelection();
setSerpKeyword(null);
beginSearch(keywords[0] ?? "", activeLocation);
runSearch(
{
projectId: input.projectId,
keywords,
locationCode: activeLocation,
resultLimit: activeResultLimit,
mode: activeMode,
},
{
onSuccess: (seedKeyword, nextRows) => {
if (nextRows.length === 0) {
setSerpKeyword(null);
return;
}
setSerpKeyword(seedKeyword);
setSerpPage(0);
},
},
);
},
);
const { filteredRows, activeFilterCount } = useKeywordFiltering({
rows,
filters: filterValues,
sortField: input.sortField,
sortDir: input.sortDir,
});
const { showApproximateMatchNotice, overviewKeyword } =
useKeywordOverviewState({
rows,
searchedKeyword,
selectedKeyword: uiState.selectedKeyword,
hasSearched,
isLoading,
lastSearchError,
keywordMode: input.keywordMode,
});
return {
activeFilterCount,
activeSerpKeyword,
beginSearch,
clearSelection,
controlsForm,
filteredRows,
filtersForm,
hasSearched,
history,
historyLoaded,
isLoading,
lastResultSource,
lastSearchError,
lastSearchKeyword,
lastSearchLocationCode,
lastUsedFallback,
mobileTab: uiState.mobileTab,
overviewKeyword,
removeHistoryItem,
resetResearch,
researchError,
researchMutationError,
runSearch,
resetFilters,
rows,
searchedKeyword,
selectedKeyword: uiState.selectedKeyword,
selectedRows,
saveMutation,
setPreferredLocationCode,
setSelectedKeyword: uiState.setSelectedKeyword,
setSearchParams,
setSerpKeyword,
serpError,
serpLoading,
serpPage,
serpQuery,
serpResults,
setMobileTab: uiState.setMobileTab,
setSerpPage,
setShowFilters: uiState.setShowFilters,
setShowSaveDialog: uiState.setShowSaveDialog,
showApproximateMatchNotice,
showFilters: uiState.showFilters,
showSaveDialog: uiState.showSaveDialog,
toggleAllRows,
toggleRowSelection,
SERP_PAGE_SIZE,
};
}