← back to Open Seo

src/client/features/keywords/page/keywordResearchFilters.tsx

174 lines

import {
  KEYWORD_INTENT_ORDER,
  parseIntentFilter,
  toggleIntentFilter,
} from "@/client/features/keywords/keywordResearchTypes";
import { INTENT_LABELS } from "@/client/features/keywords/components/IntentBadge";
import type { KeywordResearchControllerState } from "./types";

export function FilterIntentSelect({
  form,
}: {
  form: KeywordResearchControllerState["filtersForm"];
}) {
  return (
    <div
      role="group"
      aria-labelledby="keyword-intent-filter-label"
      className="rounded-lg border border-base-300 bg-base-100 p-2.5 space-y-2"
    >
      <p
        id="keyword-intent-filter-label"
        className="text-[11px] font-semibold uppercase tracking-wide text-base-content/60"
      >
        Intent
      </p>
      <form.Field name="intents">
        {(field) => {
          const selected = parseIntentFilter(field.state.value);
          return (
            <div className="flex flex-wrap gap-1.5">
              {KEYWORD_INTENT_ORDER.map((intent) => {
                const isActive = selected.includes(intent);
                return (
                  <button
                    key={intent}
                    type="button"
                    aria-pressed={isActive}
                    className={`btn btn-xs ${
                      isActive
                        ? "btn-primary"
                        : "btn-ghost border border-base-300"
                    }`}
                    onClick={() =>
                      field.handleChange(
                        toggleIntentFilter(field.state.value, intent),
                      )
                    }
                  >
                    {INTENT_LABELS[intent]}
                  </button>
                );
              })}
            </div>
          );
        }}
      </form.Field>
    </div>
  );
}

export function FilterTextInput({
  form,
  name,
  label,
  placeholder,
}: {
  form: KeywordResearchControllerState["filtersForm"];
  name: "include" | "exclude";
  label: string;
  placeholder: string;
}) {
  return (
    <label className="form-control gap-1.5">
      <span className="text-[11px] font-semibold uppercase tracking-wide text-base-content/60">
        {label}
      </span>
      <form.Field name={name}>
        {(field) => (
          <input
            className="input input-bordered input-sm bg-base-100"
            placeholder={placeholder}
            value={field.state.value}
            onChange={(event) => field.handleChange(event.target.value)}
          />
        )}
      </form.Field>
    </label>
  );
}

export function FilterRangeInputs({
  form,
  title,
  minName,
  maxName,
  step,
}: {
  form: KeywordResearchControllerState["filtersForm"];
  title: string;
  minName: "minVol" | "minCpc" | "minKd";
  maxName: "maxVol" | "maxCpc" | "maxKd";
  step?: string;
}) {
  return (
    <div className="rounded-lg border border-base-300 bg-base-100 p-2.5 space-y-2">
      <p className="text-[11px] font-semibold uppercase tracking-wide text-base-content/60">
        {title}
      </p>
      <div className="grid grid-cols-2 gap-2">
        <CompactRangeInput
          form={form}
          name={minName}
          placeholder="Min"
          step={step}
        />
        <CompactRangeInput
          form={form}
          name={maxName}
          placeholder="Max"
          step={step}
        />
      </div>
    </div>
  );
}

function CompactRangeInput({
  form,
  name,
  placeholder,
  step,
}: {
  form: KeywordResearchControllerState["filtersForm"];
  name: "minVol" | "maxVol" | "minCpc" | "maxCpc" | "minKd" | "maxKd";
  placeholder: string;
  step?: string;
}) {
  return (
    <form.Field name={name}>
      {(field) => (
        <input
          className="input input-bordered input-xs bg-base-100"
          placeholder={placeholder}
          type="number"
          step={step}
          value={field.state.value}
          onChange={(event) => field.handleChange(event.target.value)}
        />
      )}
    </form.Field>
  );
}

export function EmptyFilterResults({
  activeFilterCount,
  resetFilters,
}: {
  activeFilterCount: number;
  resetFilters: () => void;
}) {
  return (
    <div className="h-full flex flex-col items-center justify-center text-center px-4 text-base-content/50 gap-3">
      <p className="text-sm font-medium">
        No keywords match your current filters.
      </p>
      {activeFilterCount > 0 ? (
        <button className="btn btn-ghost btn-sm" onClick={resetFilters}>
          Clear filters
        </button>
      ) : null}
    </div>
  );
}