← back to Open Seo

src/client/features/keywords/hooks/useKeywordControlsForm.ts

176 lines

import { useEffect } from "react";
import { useForm } from "@tanstack/react-form";
import {
  createFormValidationErrors,
  shouldValidateFieldOnChange,
} from "@/client/lib/forms";
import {
  MAX_KEYWORDS_PER_SUBMIT,
  type KeywordMode,
  type ResultLimit,
} from "@/client/features/keywords/keywordResearchTypes";
import { parseKeywordInput } from "@/client/features/keywords/state/keywordControllerActions";

type KeywordTabValidationInput = {
  keyword: string;
  locationCode: number | undefined;
  resultLimit: ResultLimit;
  mode: KeywordMode;
  clickstream: boolean;
};

type UseKeywordControlsFormInput = {
  keywordInput: string;
  locationCode: number;
  resultLimit: ResultLimit;
  keywordMode: KeywordMode;
  clickstream: boolean;
  getOpenKeywordTabs?: () => readonly KeywordTabValidationInput[];
  keywordTabsLimit?: number;
};

export type KeywordControlsValues = {
  keyword: string;
  locationCode: number;
  resultLimit: ResultLimit;
  mode: KeywordMode;
  clickstream: boolean;
};

function getKeywordSearchValidationErrors(
  value: KeywordControlsValues,
  shouldValidateUntouchedField: boolean,
  validateEmptyKeyword: boolean,
) {
  const keywords = parseKeywordInput(value.keyword);

  if (keywords.length === 0) {
    if (!validateEmptyKeyword) return null;
    return createFormValidationErrors({
      fields: {
        keyword: "Please enter at least one keyword.",
      },
    });
  }

  if (!shouldValidateUntouchedField) return null;

  if (keywords.length > MAX_KEYWORDS_PER_SUBMIT) {
    return createFormValidationErrors({
      fields: {
        keyword: `Please enter no more than ${MAX_KEYWORDS_PER_SUBMIT} keywords (one per line).`,
      },
    });
  }

  return null;
}

function getKeywordTabCapacityError(
  value: KeywordControlsValues,
  openKeywordTabs: readonly KeywordTabValidationInput[] | undefined,
  keywordTabsLimit: number | undefined,
) {
  if (!openKeywordTabs || keywordTabsLimit == null) return null;

  const keywords = parseKeywordInput(value.keyword);
  if (keywords.length === 0) return null;

  let simulatedOpenTabs = [...openKeywordTabs];
  let skippedCount = 0;

  for (const keyword of keywords) {
    const input = {
      keyword,
      locationCode: value.locationCode,
      resultLimit: value.resultLimit,
      mode: value.mode,
      clickstream: value.clickstream,
    };
    const alreadyOpen = simulatedOpenTabs.some((tab) =>
      keywordTabMatches(tab, input),
    );
    if (alreadyOpen) continue;

    if (simulatedOpenTabs.length >= keywordTabsLimit) {
      skippedCount += 1;
      continue;
    }

    simulatedOpenTabs = [...simulatedOpenTabs, input];
  }

  if (skippedCount === 0) return null;

  return createFormValidationErrors({
    fields: {
      keyword: `${skippedCount} keyword${skippedCount === 1 ? "" : "s"} skipped - close a tab to open more (max ${keywordTabsLimit}).`,
    },
  });
}

function keywordTabMatches(
  tab: KeywordTabValidationInput,
  input: KeywordTabValidationInput,
) {
  return (
    tab.keyword === input.keyword &&
    tab.locationCode === input.locationCode &&
    tab.resultLimit === input.resultLimit &&
    tab.mode === input.mode &&
    tab.clickstream === input.clickstream
  );
}

export function useKeywordControlsForm(
  input: UseKeywordControlsFormInput,
  onSubmit: (value: KeywordControlsValues) => void,
) {
  const form = useForm({
    defaultValues: {
      keyword: input.keywordInput,
      locationCode: input.locationCode,
      resultLimit: input.resultLimit,
      mode: input.keywordMode,
      clickstream: input.clickstream,
    },
    validators: {
      onChange: ({ formApi, value }) =>
        getKeywordSearchValidationErrors(
          value,
          shouldValidateFieldOnChange(formApi, "keyword"),
          false,
        ),
      onSubmit: ({ value }) =>
        getKeywordSearchValidationErrors(value, true, true) ??
        getKeywordTabCapacityError(
          value,
          input.getOpenKeywordTabs?.(),
          input.keywordTabsLimit,
        ),
    },
    onSubmit: ({ value }) => {
      onSubmit(value);
    },
  });

  useEffect(() => {
    form.reset({
      keyword: input.keywordInput,
      locationCode: input.locationCode,
      resultLimit: input.resultLimit,
      mode: input.keywordMode,
      clickstream: input.clickstream,
    });
  }, [
    form,
    input.keywordInput,
    input.keywordMode,
    input.locationCode,
    input.resultLimit,
    input.clickstream,
  ]);

  return form;
}