← back to Open Seo

src/client/features/domain/domainFilterUtils.ts

145 lines

import type {
  KeywordsFilterValues,
  PageFilterKey,
  PagesFilterValues,
} from "@/client/features/domain/types";
import type { DomainSearchParams } from "@/types/schemas/domain";

export const KEYWORD_FILTER_FIELDS = [
  "include",
  "exclude",
  "minTraffic",
  "maxTraffic",
  "minVol",
  "maxVol",
  "minCpc",
  "maxCpc",
  "minKd",
  "maxKd",
  "minRank",
  "maxRank",
] as const satisfies ReadonlyArray<keyof KeywordsFilterValues>;

export const PAGE_FILTER_FIELDS = [
  "include",
  "exclude",
  "minTraffic",
  "maxTraffic",
  "minVol",
  "maxVol",
] as const satisfies ReadonlyArray<keyof PagesFilterValues>;

export const PAGE_SEARCH_PARAM_BY_FIELD = {
  include: "pInclude",
  exclude: "pExclude",
  minTraffic: "pMinTraffic",
  maxTraffic: "pMaxTraffic",
  minVol: "pMinVol",
  maxVol: "pMaxVol",
} as const satisfies Record<PageFilterKey, keyof DomainSearchParams>;

type SearchUpdate = Partial<DomainSearchParams>;
type FilterValues = Record<string, string>;
type FilterKey<TValues extends FilterValues> = Extract<keyof TValues, string>;

export function countKeywordFilterConditions(
  values: KeywordsFilterValues,
): number {
  return countFilterConditions(values, KEYWORD_FILTER_FIELDS);
}

export function countPageFilterConditions(values: PagesFilterValues): number {
  return countFilterConditions(values, PAGE_FILTER_FIELDS);
}

export function buildKeywordsSearchUpdate(
  values: KeywordsFilterValues,
): SearchUpdate {
  return buildFilterSearchUpdate<KeywordsFilterValues>(
    values,
    KEYWORD_FILTER_FIELDS,
    (key) => key,
  );
}

export function buildPagesSearchUpdate(
  values: PagesFilterValues,
): SearchUpdate {
  return buildFilterSearchUpdate<PagesFilterValues>(
    values,
    PAGE_FILTER_FIELDS,
    (key) => PAGE_SEARCH_PARAM_BY_FIELD[key],
  );
}

export function buildPagesClearSearchUpdate(): SearchUpdate {
  return buildFilterClearSearchUpdate<PagesFilterValues>(
    PAGE_FILTER_FIELDS,
    (key) => PAGE_SEARCH_PARAM_BY_FIELD[key],
  );
}

export function buildDomainFiltersClearSearchUpdate(): SearchUpdate {
  const update = buildFilterClearSearchUpdate<KeywordsFilterValues>(
    KEYWORD_FILTER_FIELDS,
    (key) => key,
  );
  Object.assign(
    update,
    buildFilterClearSearchUpdate<PagesFilterValues>(
      PAGE_FILTER_FIELDS,
      (key) => PAGE_SEARCH_PARAM_BY_FIELD[key],
    ),
  );
  return update;
}

function countFilterConditions<TValues extends Record<string, string>>(
  values: TValues,
  fields: ReadonlyArray<FilterKey<TValues>>,
): number {
  let n = 0;
  for (const term of values.include.split(/[,+]/)) if (term.trim()) n += 1;
  for (const term of values.exclude.split(/[,+]/)) if (term.trim()) n += 1;
  for (const key of fields) {
    if (key === "include" || key === "exclude") continue;
    if (values[key].trim() !== "") n += 1;
  }
  return n;
}

function buildFilterSearchUpdate<TValues extends FilterValues>(
  values: TValues,
  fields: ReadonlyArray<FilterKey<TValues>>,
  getParam: (key: FilterKey<TValues>) => keyof DomainSearchParams,
): SearchUpdate {
  const update: SearchUpdate = { page: undefined };
  for (const key of fields) {
    const param = getParam(key);
    const raw = values[key].trim();
    if (raw === "") {
      Object.assign(update, { [param]: undefined });
      continue;
    }
    if (key === "include" || key === "exclude") {
      Object.assign(update, { [param]: raw });
      continue;
    }
    const parsed = Number(raw);
    Object.assign(update, {
      [param]: Number.isFinite(parsed) ? parsed : undefined,
    });
  }
  return update;
}

function buildFilterClearSearchUpdate<TValues extends FilterValues>(
  fields: ReadonlyArray<FilterKey<TValues>>,
  getParam: (key: FilterKey<TValues>) => keyof DomainSearchParams,
): SearchUpdate {
  const update: SearchUpdate = { page: undefined };
  for (const key of fields)
    Object.assign(update, { [getParam(key)]: undefined });
  return update;
}