diff --git a/src/entities/Font/api/proxy/filters.ts b/src/entities/Font/api/proxy/filters.ts new file mode 100644 index 0000000..94229c8 --- /dev/null +++ b/src/entities/Font/api/proxy/filters.ts @@ -0,0 +1,83 @@ +/** + * Proxy API filters + * + * Fetches filter metadata from GlyphDiff proxy API. + * Provides type-safe response handling. + * + * @see https://api.glyphdiff.com/api/v1/filters + */ + +import { api } from '$shared/api/api'; + +/** + * Filter metadata type from backend + */ +export interface FilterMetadata { + /** Filter ID (e.g., "providers", "categories", "subsets") */ + id: string; + + /** Display name (e.g., "Font Providers", "Categories", "Character Subsets") */ + name: string; + + /** Filter description */ + description: string; + + /** Filter type */ + type: 'enum' | 'string' | 'array'; + + /** Available filter options */ + options: FilterOption[]; +} + +/** + * Filter option type + */ +export interface FilterOption { + /** Option ID (e.g., "google", "serif", "latin") */ + id: string; + + /** Display name (e.g., "Google Fonts", "Serif", "Latin") */ + name: string; + + /** Option value (e.g., "google", "serif", "latin") */ + value: string; + + /** Number of fonts with this value */ + count: number; +} + +/** + * Proxy filters API response + */ +export interface ProxyFiltersResponse { + /** Array of filter metadata */ + filters: FilterMetadata[]; +} + +/** + * Fetch filters from proxy API + * + * @returns Promise resolving to array of filter metadata + * @throws ApiError when request fails + * + * @example + * ```ts + * // Fetch all filters + * const filters = await fetchProxyFilters(); + * + * console.log(filters); // [ + * // { id: "providers", name: "Font Providers", options: [...] }, + * // { id: "categories", name: "Categories", options: [...] }, + * // { id: "subsets", name: "Character Subsets", options: [...] } + * // ] + * ``` + */ +export async function fetchProxyFilters(): Promise { + const response = await api.get('/api/v1/filters'); + + if (!response.data || !Array.isArray(response.data)) { + throw new Error('Proxy API returned invalid response'); + } + + return response.data; +} diff --git a/src/features/GetFonts/model/state/filters.svelte.ts b/src/features/GetFonts/model/state/filters.svelte.ts new file mode 100644 index 0000000..16b1927 --- /dev/null +++ b/src/features/GetFonts/model/state/filters.svelte.ts @@ -0,0 +1,135 @@ +/** + * Filters store for dynamic filter metadata + * + * Fetches and caches filter metadata from /api/v1/filters endpoint. + * Provides reactive access to filter data for providers, categories, and subsets. + * + * @example + * ```ts + * import { filtersStore } from '$features/GetFonts'; + * + * // Access filters (reactive) + * $: filters = filtersStore.filters; + * $: isLoading = filtersStore.isLoading; + * $: error = filtersStore.error; + * ``` + */ + +import { fetchProxyFilters } from '$entities/Font/api/proxy/filters'; +import type { + FilterMetadata, + FilterOption, +} from '$entities/Font/api/proxy/filters'; +import { queryClient } from '$shared/api/queryClient'; +import { + type QueryKey, + QueryObserver, + type QueryObserverOptions, + type QueryObserverResult, +} from '@tanstack/query-core'; + +/** + * Filters store wrapping TanStack Query + * + * Fetches and caches filter metadata using fetchProxyFilters() + * Provides reactive access to filter data + */ +class FiltersStore { + /** Cleanup function for effects */ + cleanup: () => void; + + /** TanStack Query result state */ + protected result = $state>({} as any); + + /** TanStack Query observer instance */ + protected observer: QueryObserver; + + /** Shared query client */ + protected qc = queryClient; + + /** + * Creates a new filters store + */ + constructor() { + this.observer = new QueryObserver(this.qc, this.getOptions()); + + // Sync TanStack Query state -> Svelte state + this.observer.subscribe(r => { + this.result = r; + }); + + // Sync Svelte state changes -> TanStack Query options + this.cleanup = $effect.root(() => { + $effect(() => { + this.observer.setOptions(this.getOptions()); + }); + }); + } + + /** + * Query key for TanStack Query caching + */ + protected getQueryKey(): QueryKey { + return ['filters'] as const; + } + + /** + * Fetch function for filter metadata + * Uses fetchProxyFilters() from proxy API + */ + protected async fetchFn(): Promise { + return await fetchProxyFilters(); + } + + /** + * TanStack Query options + */ + protected getOptions(): QueryObserverOptions { + return { + queryKey: this.getQueryKey(), + queryFn: () => this.fetchFn(), + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + }; + } + + /** + * Get all filters + */ + get filters(): FilterMetadata[] { + return this.result.data ?? []; + } + + /** + * Get loading state + */ + get isLoading(): boolean { + return this.result.isLoading; + } + + /** + * Get error state + */ + get isError(): boolean { + return this.result.isError; + } + + /** + * Get error message + */ + get error(): string | null { + return this.result.error?.message ?? null; + } + + /** + * Clean up effects and observers + */ + destroy() { + this.cleanup(); + } +} + +/** + * Singleton instance + */ +export const filtersStore = new FiltersStore();