feat: support all 94 DataForSEO countries across features (#127)
This commit is contained in:
parent
021ec830af
commit
ed65d6976c
@ -2,7 +2,7 @@
|
|||||||
"name": "open-seo",
|
"name": "open-seo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"version": "0.0.6",
|
"version": "0.0.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "AUTH_MODE=local_noauth vite dev",
|
"dev": "AUTH_MODE=local_noauth vite dev",
|
||||||
|
|||||||
9
release-notes/v0.0.7.md
Normal file
9
release-notes/v0.0.7.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
This release expands country coverage and adds bulk delete for saved keywords.
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- Support all countries tracked by DataForSEO.
|
||||||
|
- Add country selector to Domain Overview Page
|
||||||
|
- Improve bulk delete of saved keywords.
|
||||||
|
|
||||||
|
Full Changelog: https://github.com/every-app/open-seo/compare/v0.0.6...v0.0.7
|
||||||
@ -25,6 +25,7 @@ type Props = {
|
|||||||
order?: SortOrder;
|
order?: SortOrder;
|
||||||
tab: DomainActiveTab;
|
tab: DomainActiveTab;
|
||||||
search: string;
|
search: string;
|
||||||
|
locationCode: number;
|
||||||
};
|
};
|
||||||
navigate: (args: {
|
navigate: (args: {
|
||||||
search: (prev: Record<string, unknown>) => Record<string, unknown>;
|
search: (prev: Record<string, unknown>) => Record<string, unknown>;
|
||||||
@ -69,6 +70,9 @@ export function DomainOverviewPage({
|
|||||||
onSortChange={(sort) =>
|
onSortChange={(sort) =>
|
||||||
state.applySort(sort, getDefaultSortOrder(sort))
|
state.applySort(sort, getDefaultSortOrder(sort))
|
||||||
}
|
}
|
||||||
|
onLocationChange={(locationCode) =>
|
||||||
|
state.applyLocationChange(locationCode)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{state.isLoading ? (
|
{state.isLoading ? (
|
||||||
@ -148,6 +152,7 @@ export function DomainOverviewPage({
|
|||||||
}}
|
}}
|
||||||
onSearchChange={state.setPendingSearch}
|
onSearchChange={state.setPendingSearch}
|
||||||
onSaveKeywords={state.handleSaveKeywords}
|
onSaveKeywords={state.handleSaveKeywords}
|
||||||
|
canSaveKeywords={state.canSaveKeywords}
|
||||||
onSortClick={state.handleSortColumnClick}
|
onSortClick={state.handleSortColumnClick}
|
||||||
onToggleKeyword={state.toggleKeywordSelection}
|
onToggleKeyword={state.toggleKeywordSelection}
|
||||||
onToggleAllVisible={state.toggleAllVisibleKeywords}
|
onToggleAllVisible={state.toggleAllVisibleKeywords}
|
||||||
|
|||||||
@ -46,6 +46,7 @@ type Props = {
|
|||||||
onTabChange: (tab: DomainActiveTab) => void;
|
onTabChange: (tab: DomainActiveTab) => void;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onSaveKeywords: () => void;
|
onSaveKeywords: () => void;
|
||||||
|
canSaveKeywords: boolean;
|
||||||
onSortClick: (sort: DomainSortMode) => void;
|
onSortClick: (sort: DomainSortMode) => void;
|
||||||
onToggleKeyword: (keyword: string) => void;
|
onToggleKeyword: (keyword: string) => void;
|
||||||
onToggleAllVisible: () => void;
|
onToggleAllVisible: () => void;
|
||||||
@ -69,6 +70,7 @@ export function DomainResultsCard({
|
|||||||
onTabChange,
|
onTabChange,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onSaveKeywords,
|
onSaveKeywords,
|
||||||
|
canSaveKeywords,
|
||||||
onSortClick,
|
onSortClick,
|
||||||
onToggleKeyword,
|
onToggleKeyword,
|
||||||
onToggleAllVisible,
|
onToggleAllVisible,
|
||||||
@ -124,7 +126,12 @@ export function DomainResultsCard({
|
|||||||
<button
|
<button
|
||||||
className="btn btn-sm"
|
className="btn btn-sm"
|
||||||
onClick={onSaveKeywords}
|
onClick={onSaveKeywords}
|
||||||
disabled={selectedKeywords.size === 0}
|
disabled={selectedKeywords.size === 0 || !canSaveKeywords}
|
||||||
|
title={
|
||||||
|
!canSaveKeywords && selectedKeywords.size > 0
|
||||||
|
? "Re-run search to save keywords for the selected location"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Save className="size-4" /> Save Keywords
|
<Save className="size-4" /> Save Keywords
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import { getFieldError, getFormError } from "@/client/lib/forms";
|
|||||||
import type { useDomainOverviewController } from "@/client/features/domain/useDomainOverviewController";
|
import type { useDomainOverviewController } from "@/client/features/domain/useDomainOverviewController";
|
||||||
import { toSortMode } from "@/client/features/domain/utils";
|
import { toSortMode } from "@/client/features/domain/utils";
|
||||||
import type { DomainSortMode } from "@/client/features/domain/types";
|
import type { DomainSortMode } from "@/client/features/domain/types";
|
||||||
|
import { LOCATION_OPTIONS } from "@/client/features/keywords/locations";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
controlsForm: ReturnType<typeof useDomainOverviewController>["controlsForm"];
|
controlsForm: ReturnType<typeof useDomainOverviewController>["controlsForm"];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
onSubmit: (event: FormEvent) => void;
|
onSubmit: (event: FormEvent) => void;
|
||||||
onSortChange: (sort: DomainSortMode) => void;
|
onSortChange: (sort: DomainSortMode) => void;
|
||||||
|
onLocationChange: (locationCode: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DomainSearchCard({
|
export function DomainSearchCard({
|
||||||
@ -17,6 +19,7 @@ export function DomainSearchCard({
|
|||||||
isLoading,
|
isLoading,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onSortChange,
|
onSortChange,
|
||||||
|
onLocationChange,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="card bg-base-100 border border-base-300">
|
<div className="card bg-base-100 border border-base-300">
|
||||||
@ -31,7 +34,7 @@ export function DomainSearchCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
className={`input input-bordered lg:col-span-8 flex items-center gap-2 ${domainError ? "input-error" : ""}`}
|
className={`input input-bordered lg:col-span-6 flex items-center gap-2 ${domainError ? "input-error" : ""}`}
|
||||||
>
|
>
|
||||||
<Search className="size-4 text-base-content/60" />
|
<Search className="size-4 text-base-content/60" />
|
||||||
<input
|
<input
|
||||||
@ -48,6 +51,26 @@ export function DomainSearchCard({
|
|||||||
}}
|
}}
|
||||||
</controlsForm.Field>
|
</controlsForm.Field>
|
||||||
|
|
||||||
|
<controlsForm.Field name="locationCode">
|
||||||
|
{(field) => (
|
||||||
|
<select
|
||||||
|
className="select select-bordered lg:col-span-2"
|
||||||
|
value={field.state.value}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = Number(event.target.value);
|
||||||
|
field.handleChange(next);
|
||||||
|
onLocationChange(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{LOCATION_OPTIONS.map((option) => (
|
||||||
|
<option key={option.code} value={option.code}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</controlsForm.Field>
|
||||||
|
|
||||||
<controlsForm.Field name="sort">
|
<controlsForm.Field name="sort">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<select
|
<select
|
||||||
|
|||||||
@ -26,11 +26,15 @@ export function saveSelectedKeywords({
|
|||||||
filteredKeywords,
|
filteredKeywords,
|
||||||
save,
|
save,
|
||||||
projectId,
|
projectId,
|
||||||
|
locationCode,
|
||||||
|
languageCode,
|
||||||
}: {
|
}: {
|
||||||
selectedKeywords: Set<string>;
|
selectedKeywords: Set<string>;
|
||||||
filteredKeywords: DomainOverviewData["keywords"];
|
filteredKeywords: DomainOverviewData["keywords"];
|
||||||
save: (payload: Parameters<SaveMutation>[0], opts?: SaveOptions) => void;
|
save: (payload: Parameters<SaveMutation>[0], opts?: SaveOptions) => void;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
locationCode: number;
|
||||||
|
languageCode: string;
|
||||||
}) {
|
}) {
|
||||||
if (selectedKeywords.size === 0) {
|
if (selectedKeywords.size === 0) {
|
||||||
toast.error("Select at least one keyword first");
|
toast.error("Select at least one keyword first");
|
||||||
@ -44,8 +48,8 @@ export function saveSelectedKeywords({
|
|||||||
{
|
{
|
||||||
projectId,
|
projectId,
|
||||||
keywords: [...selectedKeywords],
|
keywords: [...selectedKeywords],
|
||||||
locationCode: 2840,
|
locationCode,
|
||||||
languageCode: "en",
|
languageCode,
|
||||||
metrics: selectedRows.map((row) => ({
|
metrics: selectedRows.map((row) => ({
|
||||||
keyword: row.keyword,
|
keyword: row.keyword,
|
||||||
searchVolume: row.searchVolume,
|
searchVolume: row.searchVolume,
|
||||||
|
|||||||
@ -25,6 +25,11 @@ import type {
|
|||||||
SortOrder,
|
SortOrder,
|
||||||
} from "@/client/features/domain/types";
|
} from "@/client/features/domain/types";
|
||||||
import type { DomainSearchHistoryItem } from "@/client/hooks/useDomainSearchHistory";
|
import type { DomainSearchHistoryItem } from "@/client/hooks/useDomainSearchHistory";
|
||||||
|
import {
|
||||||
|
DEFAULT_LOCATION_CODE,
|
||||||
|
getLanguageCode,
|
||||||
|
isSupportedLocationCode,
|
||||||
|
} from "@/client/features/keywords/locations";
|
||||||
|
|
||||||
export type SearchState = {
|
export type SearchState = {
|
||||||
domain: string;
|
domain: string;
|
||||||
@ -33,6 +38,7 @@ export type SearchState = {
|
|||||||
order?: SortOrder;
|
order?: SortOrder;
|
||||||
tab: DomainActiveTab;
|
tab: DomainActiveTab;
|
||||||
search: string;
|
search: string;
|
||||||
|
locationCode: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DomainNavigate = (args: {
|
type DomainNavigate = (args: {
|
||||||
@ -46,16 +52,18 @@ type DomainControlsFormAccess = {
|
|||||||
domain: string;
|
domain: string;
|
||||||
subdomains: boolean;
|
subdomains: boolean;
|
||||||
sort: DomainSortMode;
|
sort: DomainSortMode;
|
||||||
|
locationCode: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
reset: (values: {
|
reset: (values: {
|
||||||
domain: string;
|
domain: string;
|
||||||
subdomains: boolean;
|
subdomains: boolean;
|
||||||
sort: DomainSortMode;
|
sort: DomainSortMode;
|
||||||
|
locationCode: number;
|
||||||
}) => void;
|
}) => void;
|
||||||
setFieldValue: (
|
setFieldValue: (
|
||||||
field: "domain" | "subdomains" | "sort",
|
field: "domain" | "subdomains" | "sort" | "locationCode",
|
||||||
updater: string | boolean,
|
updater: string | boolean | number,
|
||||||
opts?: UpdateMetaOptions,
|
opts?: UpdateMetaOptions,
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@ -177,6 +185,7 @@ export function useSyncRouteState({
|
|||||||
domain: searchState.domain,
|
domain: searchState.domain,
|
||||||
subdomains: searchState.subdomains,
|
subdomains: searchState.subdomains,
|
||||||
sort: searchState.sort,
|
sort: searchState.sort,
|
||||||
|
locationCode: searchState.locationCode,
|
||||||
});
|
});
|
||||||
setPendingSearch(searchState.search);
|
setPendingSearch(searchState.search);
|
||||||
}, [controlsForm, searchState, setPendingSearch]);
|
}, [controlsForm, searchState, setPendingSearch]);
|
||||||
@ -185,6 +194,7 @@ export function useSyncRouteState({
|
|||||||
const raw = new URLSearchParams(window.location.search);
|
const raw = new URLSearchParams(window.location.search);
|
||||||
const rawSort = toSortMode(raw.get("sort"));
|
const rawSort = toSortMode(raw.get("sort"));
|
||||||
const rawOrder = toSortOrder(raw.get("order"));
|
const rawOrder = toSortOrder(raw.get("order"));
|
||||||
|
const rawLoc = raw.get("loc");
|
||||||
const shouldNormalize =
|
const shouldNormalize =
|
||||||
raw.get("domain") === "" ||
|
raw.get("domain") === "" ||
|
||||||
raw.get("search") === "" ||
|
raw.get("search") === "" ||
|
||||||
@ -192,7 +202,8 @@ export function useSyncRouteState({
|
|||||||
raw.get("sort") === "rank" ||
|
raw.get("sort") === "rank" ||
|
||||||
(rawOrder != null &&
|
(rawOrder != null &&
|
||||||
rawOrder === getDefaultSortOrder(rawSort ?? "rank")) ||
|
rawOrder === getDefaultSortOrder(rawSort ?? "rank")) ||
|
||||||
raw.get("tab") === "keywords";
|
raw.get("tab") === "keywords" ||
|
||||||
|
rawLoc === String(DEFAULT_LOCATION_CODE);
|
||||||
if (!shouldNormalize) return;
|
if (!shouldNormalize) return;
|
||||||
|
|
||||||
navigate({
|
navigate({
|
||||||
@ -211,6 +222,10 @@ export function useSyncRouteState({
|
|||||||
? undefined
|
? undefined
|
||||||
: prev.order,
|
: prev.order,
|
||||||
tab: prev.tab === "keywords" ? undefined : prev.tab,
|
tab: prev.tab === "keywords" ? undefined : prev.tab,
|
||||||
|
loc:
|
||||||
|
prev.loc != null && Number(prev.loc) === DEFAULT_LOCATION_CODE
|
||||||
|
? undefined
|
||||||
|
: prev.loc,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
replace: true,
|
replace: true,
|
||||||
@ -249,11 +264,11 @@ export function useSearchRunner({
|
|||||||
controlsForm: ControlsFormLike;
|
controlsForm: ControlsFormLike;
|
||||||
setPendingSearch: (value: string) => void;
|
setPendingSearch: (value: string) => void;
|
||||||
setSearchParams: (
|
setSearchParams: (
|
||||||
updates: Record<string, string | boolean | undefined>,
|
updates: Record<string, string | number | boolean | undefined>,
|
||||||
) => void;
|
) => void;
|
||||||
domainMutation: ReturnType<typeof useDomainLookupMutation>;
|
domainMutation: ReturnType<typeof useDomainLookupMutation>;
|
||||||
addSearch: (item: Omit<DomainSearchHistoryItem, "timestamp">) => void;
|
addSearch: (item: Omit<DomainSearchHistoryItem, "timestamp">) => void;
|
||||||
setOverview: (value: DomainOverviewData) => void;
|
setOverview: (value: DomainOverviewData, locationCode: number) => void;
|
||||||
setSelectedKeywords: Dispatch<SetStateAction<Set<string>>>;
|
setSelectedKeywords: Dispatch<SetStateAction<Set<string>>>;
|
||||||
currentState: SearchState;
|
currentState: SearchState;
|
||||||
currentSortOrder: SortOrder;
|
currentSortOrder: SortOrder;
|
||||||
@ -266,6 +281,12 @@ export function useSearchRunner({
|
|||||||
const activeOrder = params?.order ?? currentSortOrder;
|
const activeOrder = params?.order ?? currentSortOrder;
|
||||||
const activeTab = params?.tab ?? currentState.tab;
|
const activeTab = params?.tab ?? currentState.tab;
|
||||||
const activeSearch = params?.search ?? currentState.search;
|
const activeSearch = params?.search ?? currentState.search;
|
||||||
|
const rawLocationCode =
|
||||||
|
params?.locationCode ?? values.locationCode ?? currentState.locationCode;
|
||||||
|
const activeLocationCode = isSupportedLocationCode(rawLocationCode)
|
||||||
|
? rawLocationCode
|
||||||
|
: DEFAULT_LOCATION_CODE;
|
||||||
|
const activeLanguageCode = getLanguageCode(activeLocationCode);
|
||||||
const target = normalizeDomainTarget(rawTarget);
|
const target = normalizeDomainTarget(rawTarget);
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
@ -276,6 +297,7 @@ export function useSearchRunner({
|
|||||||
controlsForm.setFieldValue("domain", target);
|
controlsForm.setFieldValue("domain", target);
|
||||||
controlsForm.setFieldValue("subdomains", activeSubdomains);
|
controlsForm.setFieldValue("subdomains", activeSubdomains);
|
||||||
controlsForm.setFieldValue("sort", activeSort);
|
controlsForm.setFieldValue("sort", activeSort);
|
||||||
|
controlsForm.setFieldValue("locationCode", activeLocationCode);
|
||||||
|
|
||||||
setSearchParams({
|
setSearchParams({
|
||||||
domain: target,
|
domain: target,
|
||||||
@ -284,23 +306,28 @@ export function useSearchRunner({
|
|||||||
order: toSortOrderSearchParam(activeSort, activeOrder),
|
order: toSortOrderSearchParam(activeSort, activeOrder),
|
||||||
tab: activeTab === "keywords" ? undefined : activeTab,
|
tab: activeTab === "keywords" ? undefined : activeTab,
|
||||||
search: activeSearch.trim() || undefined,
|
search: activeSearch.trim() || undefined,
|
||||||
|
loc:
|
||||||
|
activeLocationCode === DEFAULT_LOCATION_CODE
|
||||||
|
? undefined
|
||||||
|
: activeLocationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await domainMutation.mutateAsync({
|
const response = await domainMutation.mutateAsync({
|
||||||
domain: target,
|
domain: target,
|
||||||
includeSubdomains: activeSubdomains,
|
includeSubdomains: activeSubdomains,
|
||||||
locationCode: 2840,
|
locationCode: activeLocationCode,
|
||||||
languageCode: "en",
|
languageCode: activeLanguageCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
captureClientEvent("domain_overview:search_complete", {
|
captureClientEvent("domain_overview:search_complete", {
|
||||||
sort_mode: activeSort,
|
sort_mode: activeSort,
|
||||||
include_subdomains: activeSubdomains,
|
include_subdomains: activeSubdomains,
|
||||||
result_count: response.keywords.length,
|
result_count: response.keywords.length,
|
||||||
|
location_code: activeLocationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
setOverview(response);
|
setOverview(response, activeLocationCode);
|
||||||
setSelectedKeywords(new Set());
|
setSelectedKeywords(new Set());
|
||||||
addSearch({
|
addSearch({
|
||||||
domain: target,
|
domain: target,
|
||||||
@ -308,6 +335,7 @@ export function useSearchRunner({
|
|||||||
sort: activeSort,
|
sort: activeSort,
|
||||||
tab: activeTab,
|
tab: activeTab,
|
||||||
search: activeSearch.trim() || undefined,
|
search: activeSearch.trim() || undefined,
|
||||||
|
locationCode: activeLocationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.hasData) {
|
if (!response.hasData) {
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export type DomainControlsValues = {
|
|||||||
domain: string;
|
domain: string;
|
||||||
subdomains: boolean;
|
subdomains: boolean;
|
||||||
sort: "rank" | "traffic" | "volume" | "score" | "cpc";
|
sort: "rank" | "traffic" | "volume" | "score" | "cpc";
|
||||||
|
locationCode: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DomainSortMode = DomainControlsValues["sort"];
|
export type DomainSortMode = DomainControlsValues["sort"];
|
||||||
@ -74,4 +75,5 @@ export type DomainHistoryItem = {
|
|||||||
sort: DomainSortMode;
|
sort: DomainSortMode;
|
||||||
tab: DomainActiveTab;
|
tab: DomainActiveTab;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
locationCode?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,6 +32,11 @@ import {
|
|||||||
useSyncRouteState,
|
useSyncRouteState,
|
||||||
type SearchState,
|
type SearchState,
|
||||||
} from "@/client/features/domain/domainOverviewControllerInternals";
|
} from "@/client/features/domain/domainOverviewControllerInternals";
|
||||||
|
import {
|
||||||
|
DEFAULT_LOCATION_CODE,
|
||||||
|
getLanguageCode,
|
||||||
|
isSupportedLocationCode,
|
||||||
|
} from "@/client/features/keywords/locations";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -51,7 +56,7 @@ type DomainControlsFormApi = {
|
|||||||
reset: (values: DomainControlsValues) => void;
|
reset: (values: DomainControlsValues) => void;
|
||||||
setFieldValue: (
|
setFieldValue: (
|
||||||
field: keyof DomainControlsValues,
|
field: keyof DomainControlsValues,
|
||||||
value: string | boolean,
|
value: string | boolean | number,
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -107,6 +112,9 @@ export function useDomainOverviewController({
|
|||||||
}: Params) {
|
}: Params) {
|
||||||
const [pendingSearch, setPendingSearch] = useState(searchState.search);
|
const [pendingSearch, setPendingSearch] = useState(searchState.search);
|
||||||
const [overview, setOverview] = useState<DomainOverviewData | null>(null);
|
const [overview, setOverview] = useState<DomainOverviewData | null>(null);
|
||||||
|
const [overviewLocationCode, setOverviewLocationCode] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
const [selectedKeywords, setSelectedKeywords] = useState<Set<string>>(
|
const [selectedKeywords, setSelectedKeywords] = useState<Set<string>>(
|
||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
@ -138,6 +146,7 @@ export function useDomainOverviewController({
|
|||||||
domain: searchState.domain,
|
domain: searchState.domain,
|
||||||
subdomains: searchState.subdomains,
|
subdomains: searchState.subdomains,
|
||||||
sort: searchState.sort,
|
sort: searchState.sort,
|
||||||
|
locationCode: searchState.locationCode,
|
||||||
},
|
},
|
||||||
validators: {
|
validators: {
|
||||||
onChange: ({ formApi, value }) =>
|
onChange: ({ formApi, value }) =>
|
||||||
@ -156,6 +165,7 @@ export function useDomainOverviewController({
|
|||||||
order: currentSortOrder,
|
order: currentSortOrder,
|
||||||
tab: searchState.tab,
|
tab: searchState.tab,
|
||||||
search: searchState.search,
|
search: searchState.search,
|
||||||
|
locationCode: value.locationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
formApi.setErrorMap({
|
formApi.setErrorMap({
|
||||||
@ -188,7 +198,10 @@ export function useDomainOverviewController({
|
|||||||
setSearchParams,
|
setSearchParams,
|
||||||
domainMutation,
|
domainMutation,
|
||||||
addSearch,
|
addSearch,
|
||||||
setOverview: (value) => setOverview(value),
|
setOverview: (value, locationCode) => {
|
||||||
|
setOverview(value);
|
||||||
|
setOverviewLocationCode(locationCode);
|
||||||
|
},
|
||||||
setSelectedKeywords,
|
setSelectedKeywords,
|
||||||
currentState: searchState,
|
currentState: searchState,
|
||||||
currentSortOrder,
|
currentSortOrder,
|
||||||
@ -199,6 +212,7 @@ export function useDomainOverviewController({
|
|||||||
currentSortOrder,
|
currentSortOrder,
|
||||||
currentState: searchState,
|
currentState: searchState,
|
||||||
dataState,
|
dataState,
|
||||||
|
overviewLocationCode,
|
||||||
projectId,
|
projectId,
|
||||||
runSearch,
|
runSearch,
|
||||||
saveMutation,
|
saveMutation,
|
||||||
@ -206,8 +220,13 @@ export function useDomainOverviewController({
|
|||||||
setSearchParams,
|
setSearchParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const canSaveKeywords =
|
||||||
|
overviewLocationCode !== null &&
|
||||||
|
overviewLocationCode === controlsForm.state.values.locationCode;
|
||||||
|
|
||||||
const resetView = useCallback(() => {
|
const resetView = useCallback(() => {
|
||||||
setOverview(null);
|
setOverview(null);
|
||||||
|
setOverviewLocationCode(null);
|
||||||
setPendingSearch("");
|
setPendingSearch("");
|
||||||
setSelectedKeywords(new Set());
|
setSelectedKeywords(new Set());
|
||||||
setShowFilters(false);
|
setShowFilters(false);
|
||||||
@ -218,6 +237,7 @@ export function useDomainOverviewController({
|
|||||||
controlsForm,
|
controlsForm,
|
||||||
isLoading: domainMutation.isPending,
|
isLoading: domainMutation.isPending,
|
||||||
overview,
|
overview,
|
||||||
|
canSaveKeywords,
|
||||||
history,
|
history,
|
||||||
historyLoaded,
|
historyLoaded,
|
||||||
removeHistoryItem,
|
removeHistoryItem,
|
||||||
@ -241,6 +261,7 @@ function useDomainControllerHandlers({
|
|||||||
currentSortOrder,
|
currentSortOrder,
|
||||||
currentState,
|
currentState,
|
||||||
dataState,
|
dataState,
|
||||||
|
overviewLocationCode,
|
||||||
projectId,
|
projectId,
|
||||||
runSearch,
|
runSearch,
|
||||||
saveMutation,
|
saveMutation,
|
||||||
@ -251,6 +272,7 @@ function useDomainControllerHandlers({
|
|||||||
currentSortOrder: SortOrder;
|
currentSortOrder: SortOrder;
|
||||||
currentState: SearchState;
|
currentState: SearchState;
|
||||||
dataState: ReturnType<typeof useOverviewDataState>;
|
dataState: ReturnType<typeof useOverviewDataState>;
|
||||||
|
overviewLocationCode: number | null;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
runSearch: ReturnType<typeof useSearchRunner>;
|
runSearch: ReturnType<typeof useSearchRunner>;
|
||||||
saveMutation: ReturnType<typeof useSaveKeywordsMutation>;
|
saveMutation: ReturnType<typeof useSaveKeywordsMutation>;
|
||||||
@ -270,6 +292,20 @@ function useDomainControllerHandlers({
|
|||||||
[controlsForm, setSearchParams],
|
[controlsForm, setSearchParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const applyLocationChange = useCallback(
|
||||||
|
(nextLocationCode: number) => {
|
||||||
|
if (!isSupportedLocationCode(nextLocationCode)) return;
|
||||||
|
controlsForm.setFieldValue("locationCode", nextLocationCode);
|
||||||
|
setSearchParams({
|
||||||
|
loc:
|
||||||
|
nextLocationCode === DEFAULT_LOCATION_CODE
|
||||||
|
? undefined
|
||||||
|
: nextLocationCode,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[controlsForm, setSearchParams],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSortColumnClick = useCallback(
|
const handleSortColumnClick = useCallback(
|
||||||
(nextSort: DomainSortMode) => {
|
(nextSort: DomainSortMode) => {
|
||||||
const nextOrder =
|
const nextOrder =
|
||||||
@ -283,19 +319,28 @@ function useDomainControllerHandlers({
|
|||||||
[applySort, currentSortOrder, currentState.sort],
|
[applySort, currentSortOrder, currentState.sort],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSaveKeywords = () =>
|
const handleSaveKeywords = () => {
|
||||||
|
if (overviewLocationCode === null) return;
|
||||||
saveSelectedKeywords({
|
saveSelectedKeywords({
|
||||||
selectedKeywords,
|
selectedKeywords,
|
||||||
filteredKeywords: dataState.filteredKeywords,
|
filteredKeywords: dataState.filteredKeywords,
|
||||||
save: saveMutation.mutate,
|
save: saveMutation.mutate,
|
||||||
projectId,
|
projectId,
|
||||||
|
locationCode: overviewLocationCode,
|
||||||
|
languageCode: getLanguageCode(overviewLocationCode),
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleHistorySelect = (item: DomainSearchHistoryItem) => {
|
const handleHistorySelect = (item: DomainSearchHistoryItem) => {
|
||||||
|
const historyLocation =
|
||||||
|
item.locationCode != null && isSupportedLocationCode(item.locationCode)
|
||||||
|
? item.locationCode
|
||||||
|
: DEFAULT_LOCATION_CODE;
|
||||||
controlsForm.reset({
|
controlsForm.reset({
|
||||||
domain: item.domain,
|
domain: item.domain,
|
||||||
subdomains: item.subdomains,
|
subdomains: item.subdomains,
|
||||||
sort: item.sort,
|
sort: item.sort,
|
||||||
|
locationCode: historyLocation,
|
||||||
});
|
});
|
||||||
void runSearch({
|
void runSearch({
|
||||||
domain: item.domain,
|
domain: item.domain,
|
||||||
@ -304,6 +349,7 @@ function useDomainControllerHandlers({
|
|||||||
order: getDefaultSortOrder(item.sort),
|
order: getDefaultSortOrder(item.sort),
|
||||||
tab: item.tab,
|
tab: item.tab,
|
||||||
search: item.search ?? "",
|
search: item.search ?? "",
|
||||||
|
locationCode: historyLocation,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -314,6 +360,7 @@ function useDomainControllerHandlers({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
applySort,
|
applySort,
|
||||||
|
applyLocationChange,
|
||||||
handleSortColumnClick,
|
handleSortColumnClick,
|
||||||
handleSaveKeywords,
|
handleSaveKeywords,
|
||||||
runSearch,
|
runSearch,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useState } from "react";
|
|||||||
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";
|
||||||
|
import { DEFAULT_LOCATION_CODE } from "@/client/features/keywords/locations";
|
||||||
import { researchKeywords } from "@/serverFunctions/keywords";
|
import { researchKeywords } from "@/serverFunctions/keywords";
|
||||||
import type {
|
import type {
|
||||||
KeywordMode,
|
KeywordMode,
|
||||||
@ -25,7 +26,9 @@ export function useKeywordResearchData(addSearch: AddSearchFn) {
|
|||||||
useState<KeywordSource>("related");
|
useState<KeywordSource>("related");
|
||||||
const [lastUsedFallback, setLastUsedFallback] = useState(false);
|
const [lastUsedFallback, setLastUsedFallback] = useState(false);
|
||||||
const [lastSearchKeyword, setLastSearchKeyword] = useState("");
|
const [lastSearchKeyword, setLastSearchKeyword] = useState("");
|
||||||
const [lastSearchLocationCode, setLastSearchLocationCode] = useState(2840);
|
const [lastSearchLocationCode, setLastSearchLocationCode] = useState(
|
||||||
|
DEFAULT_LOCATION_CODE,
|
||||||
|
);
|
||||||
const [researchError, setResearchError] = useState<string | null>(null);
|
const [researchError, setResearchError] = useState<string | null>(null);
|
||||||
const [searchedKeyword, setSearchedKeyword] = useState("");
|
const [searchedKeyword, setSearchedKeyword] = useState("");
|
||||||
|
|
||||||
@ -56,7 +59,7 @@ export function useKeywordResearchData(addSearch: AddSearchFn) {
|
|||||||
setLastResultSource("related");
|
setLastResultSource("related");
|
||||||
setLastUsedFallback(false);
|
setLastUsedFallback(false);
|
||||||
setLastSearchKeyword("");
|
setLastSearchKeyword("");
|
||||||
setLastSearchLocationCode(2840);
|
setLastSearchLocationCode(DEFAULT_LOCATION_CODE);
|
||||||
setResearchError(null);
|
setResearchError(null);
|
||||||
setSearchedKeyword("");
|
setSearchedKeyword("");
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,53 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* DataForSEO-supported countries.
|
||||||
|
*
|
||||||
|
* Source: https://cdn.dataforseo.com/v3/locations/locations_and_languages_dataforseo_labs_2026_04_06.csv
|
||||||
|
*
|
||||||
|
* For countries with multiple Google-supported languages, we pick the
|
||||||
|
* language with the largest keyword corpus (the primary search market)
|
||||||
|
* as the default. DataForSEO Labs APIs accept a single location_code +
|
||||||
|
* language_code pair per request, so we expose one entry per country.
|
||||||
|
*
|
||||||
|
* Entries are sorted alphabetically by country name; pick US as the
|
||||||
|
* product-wide default via DEFAULT_LOCATION_CODE below.
|
||||||
|
*/
|
||||||
export const DEFAULT_LOCATION_CODE = 2840;
|
export const DEFAULT_LOCATION_CODE = 2840;
|
||||||
|
|
||||||
export const LOCATION_OPTIONS = [
|
export const LOCATION_OPTIONS = [
|
||||||
{ code: 2840, label: "United States", shortLabel: "US", languageCode: "en" },
|
{ code: 2008, label: "Albania", shortLabel: "AL", languageCode: "sq" },
|
||||||
{ code: 2826, label: "United Kingdom", shortLabel: "UK", languageCode: "en" },
|
{ code: 2012, label: "Algeria", shortLabel: "DZ", languageCode: "fr" },
|
||||||
{ code: 2124, label: "Canada", shortLabel: "CA", languageCode: "en" },
|
{ code: 2024, label: "Angola", shortLabel: "AO", languageCode: "pt" },
|
||||||
{ code: 2036, label: "Australia", shortLabel: "AU", languageCode: "en" },
|
|
||||||
{ code: 2372, label: "Ireland", shortLabel: "IE", languageCode: "en" },
|
|
||||||
{ code: 2554, label: "New Zealand", shortLabel: "NZ", languageCode: "en" },
|
|
||||||
{ code: 2356, label: "India", shortLabel: "IN", languageCode: "en" },
|
|
||||||
{ code: 2702, label: "Singapore", shortLabel: "SG", languageCode: "en" },
|
|
||||||
{ code: 2710, label: "South Africa", shortLabel: "ZA", languageCode: "en" },
|
|
||||||
{ code: 2608, label: "Philippines", shortLabel: "PH", languageCode: "en" },
|
|
||||||
{ code: 2276, label: "Germany", shortLabel: "DE", languageCode: "de" },
|
|
||||||
{ code: 2250, label: "France", shortLabel: "FR", languageCode: "fr" },
|
|
||||||
{ code: 2528, label: "Netherlands", shortLabel: "NL", languageCode: "nl" },
|
|
||||||
{ code: 2724, label: "Spain", shortLabel: "ES", languageCode: "es" },
|
|
||||||
{ code: 2380, label: "Italy", shortLabel: "IT", languageCode: "it" },
|
|
||||||
{ code: 2620, label: "Portugal", shortLabel: "PT", languageCode: "pt" },
|
|
||||||
{ code: 2040, label: "Austria", shortLabel: "AT", languageCode: "de" },
|
|
||||||
{ code: 2756, label: "Switzerland", shortLabel: "CH", languageCode: "de" },
|
|
||||||
{ code: 2752, label: "Sweden", shortLabel: "SE", languageCode: "sv" },
|
|
||||||
{ code: 2578, label: "Norway", shortLabel: "NO", languageCode: "nb" },
|
|
||||||
{ code: 2208, label: "Denmark", shortLabel: "DK", languageCode: "da" },
|
|
||||||
{ code: 2616, label: "Poland", shortLabel: "PL", languageCode: "pl" },
|
|
||||||
{ code: 2203, label: "Czechia", shortLabel: "CZ", languageCode: "cs" },
|
|
||||||
{ code: 2642, label: "Romania", shortLabel: "RO", languageCode: "ro" },
|
|
||||||
{ code: 2792, label: "Turkey", shortLabel: "TR", languageCode: "tr" },
|
|
||||||
{ code: 2300, label: "Greece", shortLabel: "GR", languageCode: "el" },
|
|
||||||
{ code: 2348, label: "Hungary", shortLabel: "HU", languageCode: "hu" },
|
|
||||||
{ code: 2076, label: "Brazil", shortLabel: "BR", languageCode: "pt" },
|
|
||||||
{ code: 2484, label: "Mexico", shortLabel: "MX", languageCode: "es" },
|
|
||||||
{ code: 2032, label: "Argentina", shortLabel: "AR", languageCode: "es" },
|
{ code: 2032, label: "Argentina", shortLabel: "AR", languageCode: "es" },
|
||||||
{ code: 2170, label: "Colombia", shortLabel: "CO", languageCode: "es" },
|
{ code: 2051, label: "Armenia", shortLabel: "AM", languageCode: "hy" },
|
||||||
|
{ code: 2036, label: "Australia", shortLabel: "AU", languageCode: "en" },
|
||||||
|
{ code: 2040, label: "Austria", shortLabel: "AT", languageCode: "de" },
|
||||||
|
{ code: 2031, label: "Azerbaijan", shortLabel: "AZ", languageCode: "az" },
|
||||||
|
{ code: 2048, label: "Bahrain", shortLabel: "BH", languageCode: "ar" },
|
||||||
|
{ code: 2050, label: "Bangladesh", shortLabel: "BD", languageCode: "bn" },
|
||||||
|
{ code: 2056, label: "Belgium", shortLabel: "BE", languageCode: "nl" },
|
||||||
|
{ code: 2068, label: "Bolivia", shortLabel: "BO", languageCode: "es" },
|
||||||
|
{
|
||||||
|
code: 2070,
|
||||||
|
label: "Bosnia and Herzegovina",
|
||||||
|
shortLabel: "BA",
|
||||||
|
languageCode: "bs",
|
||||||
|
},
|
||||||
|
{ code: 2076, label: "Brazil", shortLabel: "BR", languageCode: "pt" },
|
||||||
|
{ code: 2100, label: "Bulgaria", shortLabel: "BG", languageCode: "bg" },
|
||||||
|
{ code: 2854, label: "Burkina Faso", shortLabel: "BF", languageCode: "fr" },
|
||||||
|
{ code: 2116, label: "Cambodia", shortLabel: "KH", languageCode: "en" },
|
||||||
|
{ code: 2120, label: "Cameroon", shortLabel: "CM", languageCode: "fr" },
|
||||||
|
{ code: 2124, label: "Canada", shortLabel: "CA", languageCode: "en" },
|
||||||
{ code: 2152, label: "Chile", shortLabel: "CL", languageCode: "es" },
|
{ code: 2152, label: "Chile", shortLabel: "CL", languageCode: "es" },
|
||||||
{ code: 2604, label: "Peru", shortLabel: "PE", languageCode: "es" },
|
{ code: 2170, label: "Colombia", shortLabel: "CO", languageCode: "es" },
|
||||||
{ code: 2392, label: "Japan", shortLabel: "JP", languageCode: "ja" },
|
{ code: 2188, label: "Costa Rica", shortLabel: "CR", languageCode: "es" },
|
||||||
{ code: 2410, label: "South Korea", shortLabel: "KR", languageCode: "ko" },
|
{ code: 2384, label: "Cote d'Ivoire", shortLabel: "CI", languageCode: "fr" },
|
||||||
|
{ code: 2191, label: "Croatia", shortLabel: "HR", languageCode: "hr" },
|
||||||
|
{ code: 2196, label: "Cyprus", shortLabel: "CY", languageCode: "el" },
|
||||||
|
{ code: 2203, label: "Czechia", shortLabel: "CZ", languageCode: "cs" },
|
||||||
|
{ code: 2208, label: "Denmark", shortLabel: "DK", languageCode: "da" },
|
||||||
|
{ code: 2218, label: "Ecuador", shortLabel: "EC", languageCode: "es" },
|
||||||
|
{ code: 2818, label: "Egypt", shortLabel: "EG", languageCode: "ar" },
|
||||||
|
{ code: 2222, label: "El Salvador", shortLabel: "SV", languageCode: "es" },
|
||||||
|
{ code: 2233, label: "Estonia", shortLabel: "EE", languageCode: "et" },
|
||||||
|
{ code: 2246, label: "Finland", shortLabel: "FI", languageCode: "fi" },
|
||||||
|
{ code: 2250, label: "France", shortLabel: "FR", languageCode: "fr" },
|
||||||
|
{ code: 2276, label: "Germany", shortLabel: "DE", languageCode: "de" },
|
||||||
|
{ code: 2288, label: "Ghana", shortLabel: "GH", languageCode: "en" },
|
||||||
|
{ code: 2300, label: "Greece", shortLabel: "GR", languageCode: "el" },
|
||||||
|
{ code: 2320, label: "Guatemala", shortLabel: "GT", languageCode: "es" },
|
||||||
|
{ code: 2344, label: "Hong Kong", shortLabel: "HK", languageCode: "zh-TW" },
|
||||||
|
{ code: 2348, label: "Hungary", shortLabel: "HU", languageCode: "hu" },
|
||||||
|
{ code: 2356, label: "India", shortLabel: "IN", languageCode: "en" },
|
||||||
{ code: 2360, label: "Indonesia", shortLabel: "ID", languageCode: "id" },
|
{ code: 2360, label: "Indonesia", shortLabel: "ID", languageCode: "id" },
|
||||||
{ code: 2458, label: "Malaysia", shortLabel: "MY", languageCode: "ms" },
|
{ code: 2372, label: "Ireland", shortLabel: "IE", languageCode: "en" },
|
||||||
|
{ code: 2376, label: "Israel", shortLabel: "IL", languageCode: "he" },
|
||||||
|
{ code: 2380, label: "Italy", shortLabel: "IT", languageCode: "it" },
|
||||||
|
{ code: 2392, label: "Japan", shortLabel: "JP", languageCode: "ja" },
|
||||||
|
{ code: 2400, label: "Jordan", shortLabel: "JO", languageCode: "ar" },
|
||||||
|
{ code: 2398, label: "Kazakhstan", shortLabel: "KZ", languageCode: "ru" },
|
||||||
|
{ code: 2404, label: "Kenya", shortLabel: "KE", languageCode: "en" },
|
||||||
|
{ code: 2428, label: "Latvia", shortLabel: "LV", languageCode: "lv" },
|
||||||
|
{ code: 2440, label: "Lithuania", shortLabel: "LT", languageCode: "lt" },
|
||||||
|
{ code: 2458, label: "Malaysia", shortLabel: "MY", languageCode: "en" },
|
||||||
|
{ code: 2470, label: "Malta", shortLabel: "MT", languageCode: "en" },
|
||||||
|
{ code: 2484, label: "Mexico", shortLabel: "MX", languageCode: "es" },
|
||||||
|
{ code: 2498, label: "Moldova", shortLabel: "MD", languageCode: "ro" },
|
||||||
|
{ code: 2492, label: "Monaco", shortLabel: "MC", languageCode: "fr" },
|
||||||
|
{ code: 2504, label: "Morocco", shortLabel: "MA", languageCode: "ar" },
|
||||||
|
{
|
||||||
|
code: 2104,
|
||||||
|
label: "Myanmar (Burma)",
|
||||||
|
shortLabel: "MM",
|
||||||
|
languageCode: "en",
|
||||||
|
},
|
||||||
|
{ code: 2528, label: "Netherlands", shortLabel: "NL", languageCode: "nl" },
|
||||||
|
{ code: 2554, label: "New Zealand", shortLabel: "NZ", languageCode: "en" },
|
||||||
|
{ code: 2558, label: "Nicaragua", shortLabel: "NI", languageCode: "es" },
|
||||||
|
{ code: 2566, label: "Nigeria", shortLabel: "NG", languageCode: "en" },
|
||||||
|
{
|
||||||
|
code: 2807,
|
||||||
|
label: "North Macedonia",
|
||||||
|
shortLabel: "MK",
|
||||||
|
languageCode: "mk",
|
||||||
|
},
|
||||||
|
{ code: 2578, label: "Norway", shortLabel: "NO", languageCode: "nb" },
|
||||||
|
{ code: 2586, label: "Pakistan", shortLabel: "PK", languageCode: "en" },
|
||||||
|
{ code: 2591, label: "Panama", shortLabel: "PA", languageCode: "es" },
|
||||||
|
{ code: 2600, label: "Paraguay", shortLabel: "PY", languageCode: "es" },
|
||||||
|
{ code: 2604, label: "Peru", shortLabel: "PE", languageCode: "es" },
|
||||||
|
{ code: 2608, label: "Philippines", shortLabel: "PH", languageCode: "en" },
|
||||||
|
{ code: 2616, label: "Poland", shortLabel: "PL", languageCode: "pl" },
|
||||||
|
{ code: 2620, label: "Portugal", shortLabel: "PT", languageCode: "pt" },
|
||||||
|
{ code: 2642, label: "Romania", shortLabel: "RO", languageCode: "ro" },
|
||||||
|
{ code: 2682, label: "Saudi Arabia", shortLabel: "SA", languageCode: "ar" },
|
||||||
|
{ code: 2686, label: "Senegal", shortLabel: "SN", languageCode: "fr" },
|
||||||
|
{ code: 2688, label: "Serbia", shortLabel: "RS", languageCode: "sr" },
|
||||||
|
{ code: 2702, label: "Singapore", shortLabel: "SG", languageCode: "en" },
|
||||||
|
{ code: 2703, label: "Slovakia", shortLabel: "SK", languageCode: "sk" },
|
||||||
|
{ code: 2705, label: "Slovenia", shortLabel: "SI", languageCode: "sl" },
|
||||||
|
{ code: 2710, label: "South Africa", shortLabel: "ZA", languageCode: "en" },
|
||||||
|
{ code: 2410, label: "South Korea", shortLabel: "KR", languageCode: "ko" },
|
||||||
|
{ code: 2724, label: "Spain", shortLabel: "ES", languageCode: "es" },
|
||||||
|
{ code: 2144, label: "Sri Lanka", shortLabel: "LK", languageCode: "en" },
|
||||||
|
{ code: 2752, label: "Sweden", shortLabel: "SE", languageCode: "sv" },
|
||||||
|
{ code: 2756, label: "Switzerland", shortLabel: "CH", languageCode: "de" },
|
||||||
|
{ code: 2158, label: "Taiwan", shortLabel: "TW", languageCode: "zh-TW" },
|
||||||
{ code: 2764, label: "Thailand", shortLabel: "TH", languageCode: "th" },
|
{ code: 2764, label: "Thailand", shortLabel: "TH", languageCode: "th" },
|
||||||
{ code: 2704, label: "Vietnam", shortLabel: "VN", languageCode: "vi" },
|
{ code: 2788, label: "Tunisia", shortLabel: "TN", languageCode: "ar" },
|
||||||
|
{ code: 2792, label: "Turkiye", shortLabel: "TR", languageCode: "tr" },
|
||||||
|
{ code: 2804, label: "Ukraine", shortLabel: "UA", languageCode: "uk" },
|
||||||
{
|
{
|
||||||
code: 2784,
|
code: 2784,
|
||||||
label: "United Arab Emirates",
|
label: "United Arab Emirates",
|
||||||
shortLabel: "AE",
|
shortLabel: "AE",
|
||||||
languageCode: "en",
|
languageCode: "en",
|
||||||
},
|
},
|
||||||
{ code: 2682, label: "Saudi Arabia", shortLabel: "SA", languageCode: "ar" },
|
{
|
||||||
{ code: 2050, label: "Bangladesh", shortLabel: "BD", languageCode: "bn" },
|
code: 2826,
|
||||||
|
label: "United Kingdom",
|
||||||
|
shortLabel: "UK",
|
||||||
|
languageCode: "en",
|
||||||
|
},
|
||||||
|
{ code: 2840, label: "United States", shortLabel: "US", languageCode: "en" },
|
||||||
|
{ code: 2858, label: "Uruguay", shortLabel: "UY", languageCode: "es" },
|
||||||
|
{ code: 2862, label: "Venezuela", shortLabel: "VE", languageCode: "es" },
|
||||||
|
{ code: 2704, label: "Vietnam", shortLabel: "VN", languageCode: "vi" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const LOCATION_CODES = new Set<number>(
|
const LOCATION_CODES = new Set<number>(
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export interface DomainSearchHistoryItem {
|
|||||||
sort: DomainSortMode;
|
sort: DomainSortMode;
|
||||||
tab: DomainTab;
|
tab: DomainTab;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
locationCode?: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +25,7 @@ const domainSearchHistoryItemSchema = z.object({
|
|||||||
sort: z.enum(["rank", "traffic", "volume", "score", "cpc"]),
|
sort: z.enum(["rank", "traffic", "volume", "score", "cpc"]),
|
||||||
tab: z.enum(["keywords", "pages"]),
|
tab: z.enum(["keywords", "pages"]),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
|
locationCode: z.number().int().positive().optional(),
|
||||||
timestamp: z.number(),
|
timestamp: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,6 +45,7 @@ function isSameSearch(
|
|||||||
a.subdomains === b.subdomains &&
|
a.subdomains === b.subdomains &&
|
||||||
a.sort === b.sort &&
|
a.sort === b.sort &&
|
||||||
a.tab === b.tab &&
|
a.tab === b.tab &&
|
||||||
|
a.locationCode === b.locationCode &&
|
||||||
normalizeSearchText(a.search) === normalizeSearchText(b.search)
|
normalizeSearchText(a.search) === normalizeSearchText(b.search)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,10 @@ import {
|
|||||||
toSortMode,
|
toSortMode,
|
||||||
toSortOrder,
|
toSortOrder,
|
||||||
} from "@/client/features/domain/utils";
|
} from "@/client/features/domain/utils";
|
||||||
|
import {
|
||||||
|
DEFAULT_LOCATION_CODE,
|
||||||
|
isSupportedLocationCode,
|
||||||
|
} from "@/client/features/keywords/locations";
|
||||||
import { domainSearchSchema } from "@/types/schemas/domain";
|
import { domainSearchSchema } from "@/types/schemas/domain";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_project/p/$projectId/domain")({
|
export const Route = createFileRoute("/_project/p/$projectId/domain")({
|
||||||
@ -22,6 +26,7 @@ function DomainOverviewRoute() {
|
|||||||
order,
|
order,
|
||||||
tab = "keywords",
|
tab = "keywords",
|
||||||
search = "",
|
search = "",
|
||||||
|
loc,
|
||||||
} = Route.useSearch();
|
} = Route.useSearch();
|
||||||
|
|
||||||
const normalizedSort = toSortMode(sort) ?? "rank";
|
const normalizedSort = toSortMode(sort) ?? "rank";
|
||||||
@ -29,6 +34,8 @@ function DomainOverviewRoute() {
|
|||||||
normalizedSort,
|
normalizedSort,
|
||||||
toSortOrder(order ?? null),
|
toSortOrder(order ?? null),
|
||||||
);
|
);
|
||||||
|
const normalizedLocationCode =
|
||||||
|
loc != null && isSupportedLocationCode(loc) ? loc : DEFAULT_LOCATION_CODE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DomainOverviewPage
|
<DomainOverviewPage
|
||||||
@ -43,6 +50,7 @@ function DomainOverviewRoute() {
|
|||||||
order: undefined,
|
order: undefined,
|
||||||
tab: undefined,
|
tab: undefined,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
|
loc: undefined,
|
||||||
}),
|
}),
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
@ -55,6 +63,7 @@ function DomainOverviewRoute() {
|
|||||||
order: normalizedOrder,
|
order: normalizedOrder,
|
||||||
tab,
|
tab,
|
||||||
search,
|
search,
|
||||||
|
locationCode: normalizedLocationCode,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -65,4 +65,5 @@ export const domainSearchSchema = z.object({
|
|||||||
order: z.enum(domainSortOrders).optional(),
|
order: z.enum(domainSortOrders).optional(),
|
||||||
tab: z.enum(domainTabs).optional(),
|
tab: z.enum(domainTabs).optional(),
|
||||||
search: z.string().optional(),
|
search: z.string().optional(),
|
||||||
|
loc: z.coerce.number().int().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user