/** * Unified font store * * Single source of truth for font data, powered by the proxy API. * Extends BaseFontStore for TanStack Query integration and reactivity. * * Key features: * - Provider-agnostic (proxy API handles provider logic) * - Reactive to filter changes * - Optimistic updates via TanStack Query * - Pagination support * - Provider-specific shortcuts for common operations */ import type { ProxyFontsParams } from '../../api'; import { fetchProxyFonts } from '../../api'; import type { UnifiedFont } from '../types'; import { BaseFontStore } from './baseFontStore.svelte'; /** * Unified font store wrapping TanStack Query with Svelte 5 runes * * Extends BaseFontStore to provide: * - Reactive state management * - TanStack Query integration for caching * - Dynamic parameter binding for filters * - Pagination support * * @example * ```ts * const store = new UnifiedFontStore({ * provider: 'google', * category: 'sans-serif', * limit: 50 * }); * * // Access reactive state * $effect(() => { * console.log(store.fonts); * console.log(store.isLoading); * console.log(store.pagination); * }); * * // Update parameters * store.setCategory('serif'); * store.nextPage(); * ``` */ export class UnifiedFontStore extends BaseFontStore { /** * Store pagination metadata separately from fonts * This is a workaround for TanStack Query's type system */ #paginationMetadata = $state< { total: number; limit: number; offset: number; } | null >(null); /** * Pagination metadata (derived from proxy API response) */ readonly pagination = $derived.by(() => { if (this.#paginationMetadata) { const { total, limit, offset } = this.#paginationMetadata; return { total, limit, offset, hasMore: offset + limit < total, page: Math.floor(offset / limit) + 1, totalPages: Math.ceil(total / limit), }; } return { total: 0, limit: this.params.limit || 50, offset: this.params.offset || 0, hasMore: false, page: 1, totalPages: 0, }; }); constructor(initialParams: ProxyFontsParams = {}) { super(initialParams); } /** * Query key for TanStack Query caching * Normalizes params to treat empty arrays/strings as undefined */ protected getQueryKey(params: ProxyFontsParams) { // Normalize params to treat empty arrays/strings as undefined const normalized = Object.entries(params).reduce((acc, [key, value]) => { if (value === '' || (Array.isArray(value) && value.length === 0)) { return acc; } return { ...acc, [key]: value }; }, {}); return ['unifiedFonts', normalized] as const; } /** * Fetch function that calls the proxy API * Returns the full response including pagination metadata */ protected async fetchFn(params: ProxyFontsParams): Promise { const response = await fetchProxyFonts(params); // Validate response structure if (!response) { console.error('[UnifiedFontStore] fetchProxyFonts returned undefined', { params }); throw new Error('Proxy API returned undefined response'); } if (!response.fonts) { console.error('[UnifiedFontStore] response.fonts is undefined', { response }); throw new Error('Proxy API response missing fonts array'); } if (!Array.isArray(response.fonts)) { console.error('[UnifiedFontStore] response.fonts is not an array', { fonts: response.fonts, }); throw new Error('Proxy API fonts is not an array'); } // Store pagination metadata separately for derived values this.#paginationMetadata = { total: response.total ?? 0, limit: response.limit ?? this.params.limit ?? 50, offset: response.offset ?? this.params.offset ?? 0, }; return response.fonts; } // --- Getters (proxied from BaseFontStore) --- /** * Get all fonts from current query result */ get fonts(): UnifiedFont[] { // The result.data is UnifiedFont[] (from TanStack Query) return (this.result.data as UnifiedFont[] | undefined) ?? []; } /** * Check if loading initial data */ get isLoading(): boolean { return this.result.isLoading; } /** * Check if fetching (including background refetches) */ get isFetching(): boolean { return this.result.isFetching; } /** * Check if error occurred */ get isError(): boolean { return this.result.isError; } /** * Check if result is empty (not loading and no fonts) */ get isEmpty(): boolean { return !this.isLoading && this.fonts.length === 0; } // --- Provider-specific shortcuts --- /** * Set provider filter */ setProvider(provider: 'google' | 'fontshare' | undefined) { this.setParams({ provider }); } /** * Set category filter */ setCategory(category: ProxyFontsParams['category']) { this.setParams({ category }); } /** * Set subset filter */ setSubset(subset: ProxyFontsParams['subset']) { this.setParams({ subset }); } /** * Set search query */ setSearch(search: string) { this.setParams({ q: search || undefined }); } /** * Set sort order */ setSort(sort: ProxyFontsParams['sort']) { this.setParams({ sort }); } // --- Pagination methods --- /** * Go to next page */ nextPage() { if (this.pagination.hasMore) { this.setParams({ offset: this.pagination.offset + this.pagination.limit, }); } } /** * Go to previous page */ prevPage() { if (this.pagination.page > 1) { this.setParams({ offset: this.pagination.offset - this.pagination.limit, }); } } /** * Go to specific page */ goToPage(page: number) { if (page >= 1 && page <= this.pagination.totalPages) { this.setParams({ offset: (page - 1) * this.pagination.limit, }); } } /** * Set limit (items per page) */ setLimit(limit: number) { this.setParams({ limit }); } // --- Category shortcuts (for convenience) --- get sansSerifFonts() { return this.fonts.filter(f => f.category === 'sans-serif'); } get serifFonts() { return this.fonts.filter(f => f.category === 'serif'); } get displayFonts() { return this.fonts.filter(f => f.category === 'display'); } get handwritingFonts() { return this.fonts.filter(f => f.category === 'handwriting'); } get monospaceFonts() { return this.fonts.filter(f => f.category === 'monospace'); } } /** * Factory function to create unified font store */ export function createUnifiedFontStore(params: ProxyFontsParams = {}) { return new UnifiedFontStore(params); } /** * Singleton instance for global use */ export const unifiedFontStore = new UnifiedFontStore();