← back to Open Seo
src/client/features/ai-search/useBrandLookupFilters.ts
98 lines
import { useCallback, useEffect, useState } from "react";
import { useForm, useStore } from "@tanstack/react-form";
import {
EMPTY_QUERIES_FILTERS,
EMPTY_TOP_PAGES_FILTERS,
type QueriesFilterValues,
type TopPagesFilterValues,
} from "./brandLookupFilterTypes";
import { countActiveFilters } from "./brandLookupFiltering";
// v3: the pages tab returned to provider page-level metrics after a brief
// sampled-prompt scale. Bump the prefix so local min/max filters do not carry
// between incompatible metric scales.
const STORAGE_KEY_PREFIX = "brand-lookup-filters-v3:";
type FilterValues = Record<string, string>;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function loadFromStorage<T extends FilterValues>(tab: string, fallback: T): T {
const fallbackClone = { ...fallback };
try {
const raw = localStorage.getItem(`${STORAGE_KEY_PREFIX}${tab}`);
if (!raw) return fallbackClone;
const parsed: unknown = JSON.parse(raw);
if (!isRecord(parsed)) return fallbackClone;
const result = { ...fallbackClone };
for (const key in fallback) {
const value = parsed[key];
if (typeof value === "string") {
Object.assign(result, { [key]: value });
}
}
return result;
} catch {
return fallbackClone;
}
}
function saveToStorage(tab: string, values: FilterValues) {
try {
localStorage.setItem(`${STORAGE_KEY_PREFIX}${tab}`, JSON.stringify(values));
} catch {
// storage full - silently ignore
}
}
function useTabFilters<T extends FilterValues>(tab: string, emptyValues: T) {
const [defaultValues] = useState<T>(() =>
loadFromStorage(tab, { ...emptyValues }),
);
const form = useForm({ defaultValues });
const values = useStore(form.store, (state) => state.values);
useEffect(() => {
saveToStorage(tab, values);
}, [tab, values]);
const reset = useCallback(() => {
form.reset({ ...emptyValues }, { keepDefaultValues: true });
}, [emptyValues, form]);
return {
form,
values,
reset,
activeFilterCount: countActiveFilters(values),
};
}
export function useBrandLookupFilters() {
const [showFilters, setShowFilters] = useState(false);
const pages = useTabFilters<TopPagesFilterValues>(
"pages",
EMPTY_TOP_PAGES_FILTERS,
);
const queries = useTabFilters<QueriesFilterValues>(
"queries",
EMPTY_QUERIES_FILTERS,
);
return {
pages,
queries,
showFilters,
setShowFilters,
};
}
export type BrandLookupFiltersState = ReturnType<typeof useBrandLookupFilters>;