Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | import { z } from 'zod';
import type { RecipeSearchFilters } from './types';
export const DEFAULT_FILTERS: RecipeSearchFilters = {
title: '',
categoryKey: null,
difficultyLevelKey: null,
labelKeys: [],
maxCookingTime: '',
};
/* ─── Zod validation schema ───────────────────── */
export const recipeSearchSchema = z.object({
title: z.string().max(100),
categoryKey: z.string().nullable(),
difficultyLevelKey: z.string().nullable(),
labelKeys: z.array(z.string()),
maxCookingTime: z.union([z.number().int().positive(), z.literal('')]),
});
const PARAM_TITLE = 'q';
const PARAM_CATEGORY = 'category';
const PARAM_DIFFICULTY = 'difficulty';
const PARAM_LABELS = 'labels';
const PARAM_MAX_TIME = 'maxTime';
/** Serialize filters into URLSearchParams (omitting defaults). */
export const filtersToSearchParams = (
filters: RecipeSearchFilters,
): URLSearchParams => {
const params = new URLSearchParams();
if (filters.title.trim()) params.set(PARAM_TITLE, filters.title.trim());
if (filters.categoryKey) params.set(PARAM_CATEGORY, filters.categoryKey);
if (filters.difficultyLevelKey)
params.set(PARAM_DIFFICULTY, filters.difficultyLevelKey);
if (filters.labelKeys.length > 0)
params.set(PARAM_LABELS, filters.labelKeys.join(','));
if (filters.maxCookingTime)
params.set(PARAM_MAX_TIME, String(filters.maxCookingTime));
return params;
};
/** Parse URLSearchParams back to RecipeSearchFilters. */
export const searchParamsToFilters = (
params: URLSearchParams,
): RecipeSearchFilters => {
const labelsRaw = params.get(PARAM_LABELS);
const maxTimeRaw = params.get(PARAM_MAX_TIME);
return {
title: params.get(PARAM_TITLE) ?? '',
categoryKey: params.get(PARAM_CATEGORY) ?? null,
difficultyLevelKey: params.get(PARAM_DIFFICULTY) ?? null,
labelKeys: labelsRaw ? labelsRaw.split(',').filter(Boolean) : [],
maxCookingTime: maxTimeRaw ? Number(maxTimeRaw) : '',
};
};
/** Returns true when at least one filter differs from defaults. */
export const isSearchActive = (filters: RecipeSearchFilters): boolean => {
return (
filters.title.trim() !== '' ||
filters.categoryKey !== null ||
filters.difficultyLevelKey !== null ||
filters.labelKeys.length > 0 ||
filters.maxCookingTime !== ''
);
};
/** Convert client-side filters to the GraphQL RecipeFilterInput shape. */
export const buildQueryFilter = (filters: RecipeSearchFilters) => {
if (!isSearchActive(filters)) return undefined;
return {
...(filters.title.trim() && { title: filters.title.trim() }),
...(filters.categoryKey && { categoryKey: filters.categoryKey }),
...(filters.difficultyLevelKey && {
difficultyLevelKey: filters.difficultyLevelKey,
}),
...(filters.labelKeys.length > 0 && { labelKeys: filters.labelKeys }),
...(filters.maxCookingTime && {
maxCookingTime: Number(filters.maxCookingTime),
}),
};
};
|