From 6f7e863b137987d395f213ca2d3ceeaca16e109e Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Fri, 9 Jan 2026 16:09:56 +0300 Subject: [PATCH] fix: use proper types for fetching fonts --- .../model/services/fetchFontshareFonts.ts | 402 ++++++++------ .../model/services/fetchGoogleFonts.ts | 502 ++++++++++++------ 2 files changed, 569 insertions(+), 335 deletions(-) diff --git a/src/features/FetchFonts/model/services/fetchFontshareFonts.ts b/src/features/FetchFonts/model/services/fetchFontshareFonts.ts index e7e0e8b..83c70b9 100644 --- a/src/features/FetchFonts/model/services/fetchFontshareFonts.ts +++ b/src/features/FetchFonts/model/services/fetchFontshareFonts.ts @@ -1,211 +1,269 @@ -/** - * Service for fetching Fontshare fonts - * - * Integrates with TanStack Query for caching, deduplication, - * and automatic refetching. - */ - -import { fetchFontshareFonts } from '$entities/Font/api/fontshare/fontshare'; -import { normalizeFontshareFonts } from '$entities/Font/api/normalize/normalize'; -import type { UnifiedFont } from '$entities/Font/model/types/normalize'; -import type { QueryFunction } from '@tanstack/svelte-query'; import { + type FontshareParams, + type UnifiedFont, + fetchFontshareFonts, + normalizeFontshareFonts, +} from '$entities/Font'; +import { + type CreateQueryResult, createQuery, useQueryClient, } from '@tanstack/svelte-query'; /** - * Fontshare query parameters + * Query key factory */ -export interface FontshareQueryParams { - /** Filter by categories (e.g., ["Sans", "Serif"]) */ - categories?: string[]; - /** Filter by tags (e.g., ["Branding", "Logos"]) */ - tags?: string[]; - /** Page number for pagination */ - page?: number; - /** Number of items per page */ - limit?: number; - /** Search query */ - search?: string; +function getFontshareQueryKey(params: FontshareParams) { + return ['fontshare', params] as const; } /** - * Query key factory for Fontshare - * Generates consistent query keys for cache management + * Query function */ -export function getFontshareQueryKey( - params: FontshareQueryParams, -): readonly unknown[] { - return ['fontshare', params]; -} - -/** - * Query function for fetching Fontshare fonts - * Handles caching, loading states, and errors - */ -export const fetchFontshareFontsQuery: QueryFunction< - UnifiedFont[], - readonly unknown[] -> = async ({ queryKey }) => { - const params = queryKey[1] as FontshareQueryParams; - +async function fetchFontshareFontsQuery(params: FontshareParams): Promise { try { - const response = await fetchFontshareFonts({ - categories: params.categories, - tags: params.tags, - page: params.page, - limit: params.limit, - search: params.search, - }); - - const normalizedFonts = normalizeFontshareFonts(response.items); - return normalizedFonts; + const response = await fetchFontshareFonts(params); + return normalizeFontshareFonts(response.items); } catch (error) { - // User-friendly error messages if (error instanceof Error) { if (error.message.includes('Failed to fetch')) { throw new Error( - 'Unable to connect to Fontshare. Please check your internet connection and try again.', + 'Unable to connect to Fontshare. Please check your internet connection.', ); } if (error.message.includes('404')) { throw new Error('Font not found in Fontshare catalog.'); } - throw new Error( - 'Failed to load fonts from Fontshare. Please try again later.', - ); } - throw new Error('An unexpected error occurred while fetching fonts.'); + throw new Error('Failed to load fonts from Fontshare.'); } -}; - -/** - * Create a Fontshare query hook - * Use this in Svelte components to fetch Fontshare fonts with caching - * - * @param params - Query parameters - * @returns Query result with data, loading state, and error - * - * @example - * ```svelte - * - * - * {#each fonts as font} - * - * {/each} - * ``` - */ -export function useFontshareFontsQuery( - params: FontshareQueryParams = {}, -) { - useQueryClient(); - - const query = createQuery(() => ({ - queryKey: getFontshareQueryKey(params), - queryFn: fetchFontshareFontsQuery, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - })); - - return query; } /** - * Prefetch Fontshare fonts - * Fetch fonts in background without showing loading state - * - * @param params - Query parameters for prefetch - * - * @example - * ```ts - * // Prefetch fonts when user hovers over button - * function onMouseEnter() { - * prefetchFontshareFonts({ categories: ['Sans'] }); - * } - * ``` + * Fontshare store wrapping TanStack Query with runes */ -export async function prefetchFontshareFonts( - params: FontshareQueryParams = {}, -): Promise { - const queryClient = useQueryClient(); +export class FontshareStore { + params = $state({}); + private query: CreateQueryResult; + private queryClient = useQueryClient(); - await queryClient.prefetchQuery({ - queryKey: getFontshareQueryKey(params), - queryFn: fetchFontshareFontsQuery, - staleTime: 5 * 60 * 1000, - }); -} + constructor(initialParams: FontshareParams = {}) { + this.params = initialParams; -/** - * Invalidate Fontshare cache - * Forces refetch on next query - * - * @param params - Query parameters to invalidate (all if not provided) - * - * @example - * ```ts - * // Invalidate all Fontshare cache - * invalidateFontshareFonts(); - * - * // Invalidate specific category cache - * invalidateFontshareFonts({ categories: ['Sans'] }); - * ``` - */ -export function invalidateFontshareFonts( - params?: FontshareQueryParams, -): void { - const queryClient = useQueryClient(); + // Create the query - it's already reactive + this.query = createQuery(() => ({ + queryKey: getFontshareQueryKey(this.params), + queryFn: () => fetchFontshareFontsQuery(this.params), + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + })); + } - if (params) { - queryClient.invalidateQueries({ - queryKey: getFontshareQueryKey(params), + // Proxy TanStack Query's reactive state + get fonts() { + return this.query.data ?? []; + } + + get isLoading() { + return this.query.isLoading; + } + + get isFetching() { + return this.query.isFetching; + } + + get error() { + return this.query.error; + } + + get isError() { + return this.query.isError; + } + + get isSuccess() { + return this.query.isSuccess; + } + + // Derived helpers + get hasData() { + return this.fonts.length > 0; + } + + get isEmpty() { + return !this.isLoading && this.fonts.length === 0; + } + + /** + * Update parameters - TanStack Query will automatically refetch + */ + setParams(newParams: Partial) { + this.params = { ...this.params, ...newParams }; + } + + setCategories(categories: string[]) { + this.setParams({ categories }); + } + + setTags(tags: string[]) { + this.setParams({ tags }); + } + + setSearch(search: string) { + this.setParams({ search }); + } + + setPage(page: number) { + this.setParams({ page }); + } + + setLimit(limit: number) { + this.setParams({ limit }); + } + + /** + * Manually refetch + */ + async refetch() { + await this.query.refetch(); + } + + /** + * Invalidate cache and refetch + */ + invalidate() { + this.queryClient.invalidateQueries({ + queryKey: getFontshareQueryKey(this.params), }); - } else { - queryClient.invalidateQueries({ + } + + /** + * Invalidate all Fontshare queries + */ + invalidateAll() { + this.queryClient.invalidateQueries({ queryKey: ['fontshare'], }); } + + /** + * Prefetch with different params (for hover states, pagination, etc.) + */ + async prefetch(params: FontshareParams) { + await this.queryClient.prefetchQuery({ + queryKey: getFontshareQueryKey(params), + queryFn: () => fetchFontshareFontsQuery(params), + staleTime: 5 * 60 * 1000, + }); + } + + /** + * Cancel ongoing queries + */ + cancel() { + this.queryClient.cancelQueries({ + queryKey: getFontshareQueryKey(this.params), + }); + } + + /** + * Clear cache for current params + */ + clearCache() { + this.queryClient.removeQueries({ + queryKey: getFontshareQueryKey(this.params), + }); + } + + /** + * Get cached data without triggering fetch + */ + getCachedData() { + return this.queryClient.getQueryData( + getFontshareQueryKey(this.params), + ); + } + + /** + * Set data manually (optimistic updates) + */ + setQueryData(updater: (old: UnifiedFont[] | undefined) => UnifiedFont[]) { + this.queryClient.setQueryData( + getFontshareQueryKey(this.params), + updater, + ); + } +} + +export function createFontshareStore(params: FontshareParams = {}) { + return new FontshareStore(params); } /** - * Cancel Fontshare queries - * Abort in-flight requests - * - * @param params - Query parameters to cancel (all if not provided) - * - * @example - * ```ts - * // Cancel all Fontshare queries - * cancelFontshareFontsQueries(); - * ``` + * Manager for multiple Fontshare stores */ -export function cancelFontshareFontsQueries( - params?: FontshareQueryParams, -): void { - const queryClient = useQueryClient(); +// export class FontshareManager { +// stores = $state>(new Map()); +// private queryClient = useQueryClient(); - if (params) { - queryClient.cancelQueries({ - queryKey: getFontshareQueryKey(params), - }); - } else { - queryClient.cancelQueries({ - queryKey: ['fontshare'], - }); - } -} +// getStore(params: FontshareParams = {}): FontshareStore { +// const key = JSON.stringify(params); + +// if (!this.stores.has(key)) { +// this.stores.set(key, new FontshareStore(params)); +// } + +// return this.stores.get(key)!; +// } + +// /** +// * Invalidate ALL Fontshare queries across all stores +// */ +// invalidateAll() { +// this.queryClient.invalidateQueries({ +// queryKey: ['fontshare'], +// }); +// } + +// /** +// * Prefetch fonts in background +// */ +// async prefetch(params: FontshareParams) { +// await this.queryClient.prefetchQuery({ +// queryKey: getFontshareQueryKey(params), +// queryFn: () => fetchFontshareFontsQuery(params), +// staleTime: 5 * 60 * 1000, +// }); +// } + +// /** +// * Cancel all Fontshare queries +// */ +// cancelAll() { +// this.queryClient.cancelQueries({ +// queryKey: ['fontshare'], +// }); +// } + +// /** +// * Clear all Fontshare cache +// */ +// clearAllCache() { +// this.queryClient.removeQueries({ +// queryKey: ['fontshare'], +// }); +// } + +// /** +// * Get query state for debugging +// */ +// getQueryState(params: FontshareParams) { +// return this.queryClient.getQueryState( +// getFontshareQueryKey(params), +// ); +// } +// } + +// export function createFontshareManager() { +// return new FontshareManager(); +// } +// diff --git a/src/features/FetchFonts/model/services/fetchGoogleFonts.ts b/src/features/FetchFonts/model/services/fetchGoogleFonts.ts index df346ca..64051ba 100644 --- a/src/features/FetchFonts/model/services/fetchGoogleFonts.ts +++ b/src/features/FetchFonts/model/services/fetchGoogleFonts.ts @@ -1,21 +1,16 @@ /** - * Service for fetching Google Fonts - * - * Integrates with TanStack Query for caching, deduplication, - * and automatic refetching. - * - * Uses reactive query args pattern for Svelte 5 compatibility. + * Service for fetching Google Fonts with Svelte 5 runes + TanStack Query */ - -import { fetchGoogleFonts } from '$entities/Font/api/google/googleFonts'; -import { normalizeGoogleFonts } from '$entities/Font/api/normalize/normalize'; +import { fetchGoogleFonts } from '$entities/Font'; +import { normalizeGoogleFonts } from '$entities/Font'; import type { FontCategory, FontSubset, -} from '$entities/Font/model/types'; -import type { UnifiedFont } from '$entities/Font/model/types/normalize'; -import type { QueryFunction } from '@tanstack/svelte-query'; + GoogleFontsParams, +} from '$entities/Font'; +import type { UnifiedFont } from '$entities/Font'; import { + type CreateQueryResult, createQuery, useQueryClient, } from '@tanstack/svelte-query'; @@ -23,191 +18,372 @@ import { /** * Google Fonts query parameters */ -export interface GoogleFontsQueryParams { - /** Font category filter */ - category?: FontCategory; - /** Character subset filter */ - subset?: FontSubset; - /** Sort order */ - sort?: 'popularity' | 'alpha' | 'date'; - /** Search query (for specific font) */ - search?: string; - /** Force refetch even if cached */ - forceRefetch?: boolean; +// export interface GoogleFontsParams { +// category?: FontCategory; +// subset?: FontSubset; +// sort?: 'popularity' | 'alpha' | 'date'; +// search?: string; +// forceRefetch?: boolean; +// } + +/** + * Query key factory + */ +function getGoogleFontsQueryKey(params: GoogleFontsParams) { + return ['googleFonts', params] as const; } /** - * Query key factory for Google Fonts - * Generates consistent query keys for cache management + * Query function */ -export function getGoogleFontsQueryKey( - params: GoogleFontsQueryParams, -): readonly unknown[] { - return ['googleFonts', params]; -} - -/** - * Query function for fetching Google Fonts - * Handles caching, loading states, and errors - */ -export const fetchGoogleFontsQuery: QueryFunction< - UnifiedFont[], - readonly unknown[] -> = async ({ queryKey }) => { - const params = queryKey[1] as GoogleFontsQueryParams; - +async function fetchGoogleFontsQuery(params: GoogleFontsParams): Promise { try { const response = await fetchGoogleFonts({ category: params.category, subset: params.subset, sort: params.sort, }); - - const normalizedFonts = normalizeGoogleFonts(response.items); - return normalizedFonts; + return normalizeGoogleFonts(response.items); } catch (error) { - // User-friendly error messages if (error instanceof Error) { if (error.message.includes('Failed to fetch')) { throw new Error( - 'Unable to connect to Google Fonts. Please check your internet connection and try again.', + 'Unable to connect to Google Fonts. Please check your internet connection.', ); } if (error.message.includes('404')) { throw new Error('Font not found in Google Fonts catalog.'); } - throw new Error( - 'Failed to load fonts from Google Fonts. Please try again later.', - ); } - throw new Error('An unexpected error occurred while fetching fonts.'); + throw new Error('Failed to load fonts from Google Fonts.'); } -}; - -/** - * Create a Google Fonts query hook - * Use this in Svelte components to fetch Google Fonts with caching - * - * @param params - Query parameters - * @returns Query result with data, loading state, and error - * - * @example - * ```svelte - * - * - * {#each fonts as font} - * - * {/each} - * ``` - */ -export function useGoogleFontsQuery(params: GoogleFontsQueryParams = {}) { - useQueryClient(); - - const query = createQuery(() => ({ - queryKey: getGoogleFontsQueryKey(params), - queryFn: fetchGoogleFontsQuery, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes - })); - - return query; } /** - * Prefetch Google Fonts - * Fetch fonts in background without showing loading state - * - * @param params - Query parameters for prefetch - * - * @example - * ```ts - * // Prefetch fonts when user hovers over button - * function onMouseEnter() { - * prefetchGoogleFonts({ category: 'sans-serif' }); - * } - * ``` + * Google Fonts store wrapping TanStack Query with runes */ -export async function prefetchGoogleFonts( - params: GoogleFontsQueryParams = {}, -): Promise { - const queryClient = useQueryClient(); +export class GoogleFontsStore { + params = $state({}); + private query: CreateQueryResult; + private queryClient = useQueryClient(); - await queryClient.prefetchQuery({ - queryKey: getGoogleFontsQueryKey(params), - queryFn: fetchGoogleFontsQuery, - staleTime: 5 * 60 * 1000, - }); -} + constructor(initialParams: GoogleFontsParams = {}) { + this.params = initialParams; -/** - * Invalidate Google Fonts cache - * Forces refetch on next query - * - * @param params - Query parameters to invalidate (all if not provided) - * - * @example - * ```ts - * // Invalidate all Google Fonts cache - * invalidateGoogleFonts(); - * - * // Invalidate specific category cache - * invalidateGoogleFonts({ category: 'sans-serif' }); - * ``` - */ -export function invalidateGoogleFonts( - params?: GoogleFontsQueryParams, -): void { - const queryClient = useQueryClient(); + // Create the query - automatically reactive + this.query = createQuery(() => ({ + queryKey: getGoogleFontsQueryKey(this.params), + queryFn: () => fetchGoogleFontsQuery(this.params), + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + })); + } - if (params) { - queryClient.invalidateQueries({ - queryKey: getGoogleFontsQueryKey(params), + // Proxy TanStack Query's reactive state + get fonts() { + return this.query.data ?? []; + } + + get isLoading() { + return this.query.isLoading; + } + + get isFetching() { + return this.query.isFetching; + } + + get isRefetching() { + return this.query.isRefetching; + } + + get error() { + return this.query.error; + } + + get isError() { + return this.query.isError; + } + + get isSuccess() { + return this.query.isSuccess; + } + + get status() { + return this.query.status; + } + + // Derived helpers + get hasData() { + return this.fonts.length > 0; + } + + get isEmpty() { + return !this.isLoading && this.fonts.length === 0; + } + + get fontCount() { + return this.fonts.length; + } + + // Filtered fonts by category (if you need additional client-side filtering) + 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'); + } + + /** + * Update parameters - TanStack Query will automatically refetch + */ + setParams(newParams: Partial) { + this.params = { ...this.params, ...newParams }; + } + + setCategory(category: FontCategory | undefined) { + this.setParams({ category }); + } + + setSubset(subset: FontSubset | undefined) { + this.setParams({ subset }); + } + + setSort(sort: 'popularity' | 'alpha' | 'date' | undefined) { + this.setParams({ sort }); + } + + setSearch(search: string) { + this.setParams({ search }); + } + + clearSearch() { + this.setParams({ search: undefined }); + } + + clearFilters() { + this.params = {}; + } + + /** + * Manually refetch + */ + async refetch() { + await this.query.refetch(); + } + + /** + * Invalidate cache and refetch + */ + invalidate() { + this.queryClient.invalidateQueries({ + queryKey: getGoogleFontsQueryKey(this.params), }); - } else { - queryClient.invalidateQueries({ + } + + /** + * Invalidate all Google Fonts queries + */ + invalidateAll() { + this.queryClient.invalidateQueries({ queryKey: ['googleFonts'], }); } + + /** + * Prefetch with different params (for hover states, pagination, etc.) + */ + async prefetch(params: GoogleFontsParams) { + await this.queryClient.prefetchQuery({ + queryKey: getGoogleFontsQueryKey(params), + queryFn: () => fetchGoogleFontsQuery(params), + staleTime: 5 * 60 * 1000, + }); + } + + /** + * Prefetch next category (useful for tab switching) + */ + async prefetchCategory(category: FontCategory) { + await this.prefetch({ ...this.params, category }); + } + + /** + * Cancel ongoing queries + */ + cancel() { + this.queryClient.cancelQueries({ + queryKey: getGoogleFontsQueryKey(this.params), + }); + } + + /** + * Clear cache for current params + */ + clearCache() { + this.queryClient.removeQueries({ + queryKey: getGoogleFontsQueryKey(this.params), + }); + } + + /** + * Get cached data without triggering fetch + */ + getCachedData() { + return this.queryClient.getQueryData( + getGoogleFontsQueryKey(this.params), + ); + } + + /** + * Check if data exists in cache + */ + hasCache(params?: GoogleFontsParams) { + const key = params ? getGoogleFontsQueryKey(params) : getGoogleFontsQueryKey(this.params); + return this.queryClient.getQueryData(key) !== undefined; + } + + /** + * Set data manually (optimistic updates) + */ + setQueryData(updater: (old: UnifiedFont[] | undefined) => UnifiedFont[]) { + this.queryClient.setQueryData( + getGoogleFontsQueryKey(this.params), + updater, + ); + } + + /** + * Get query state for debugging + */ + getQueryState() { + return this.queryClient.getQueryState( + getGoogleFontsQueryKey(this.params), + ); + } } /** - * Cancel Google Fonts queries - * Abort in-flight requests - * - * @param params - Query parameters to cancel (all if not provided) - * - * @example - * ```ts - * // Cancel all Google Fonts queries - * cancelGoogleFontsQueries(); - * ``` + * Factory function to create Google Fonts store */ -export function cancelGoogleFontsQueries( - params?: GoogleFontsQueryParams, -): void { - const queryClient = useQueryClient(); - - if (params) { - queryClient.cancelQueries({ - queryKey: getGoogleFontsQueryKey(params), - }); - } else { - queryClient.cancelQueries({ - queryKey: ['googleFonts'], - }); - } +export function createGoogleFontsStore(params: GoogleFontsParams = {}) { + return new GoogleFontsStore(params); } + +/** + * Manager for multiple Google Fonts stores + */ +// export class GoogleFontsManager { +// stores = $state>(new Map()); +// private queryClient = useQueryClient(); + +// /** +// * Get or create a store with specific parameters +// */ +// getStore(params: GoogleFontsParams = {}): GoogleFontsStore { +// const key = JSON.stringify(params); + +// if (!this.stores.has(key)) { +// this.stores.set(key, new GoogleFontsStore(params)); +// } + +// return this.stores.get(key)!; +// } + +// /** +// * Get store by category (convenience method) +// */ +// getStoreByCategory(category: FontCategory): GoogleFontsStore { +// return this.getStore({ category }); +// } + +// /** +// * Invalidate ALL Google Fonts queries across all stores +// */ +// invalidateAll() { +// this.queryClient.invalidateQueries({ +// queryKey: ['googleFonts'], +// }); +// } + +// /** +// * Prefetch fonts in background +// */ +// async prefetch(params: GoogleFontsParams) { +// await this.queryClient.prefetchQuery({ +// queryKey: getGoogleFontsQueryKey(params), +// queryFn: () => fetchGoogleFontsQuery(params), +// staleTime: 5 * 60 * 1000, +// }); +// } + +// /** +// * Prefetch all categories (useful on app init) +// */ +// async prefetchAllCategories() { +// const categories: FontCategory[] = ['sans-serif', 'serif', 'display', 'handwriting', 'monospace']; +// await Promise.all( +// categories.map(category => this.prefetch({ category })) +// ); +// } + +// /** +// * Cancel all Google Fonts queries +// */ +// cancelAll() { +// this.queryClient.cancelQueries({ +// queryKey: ['googleFonts'], +// }); +// } + +// /** +// * Clear all Google Fonts cache +// */ +// clearAllCache() { +// this.queryClient.removeQueries({ +// queryKey: ['googleFonts'], +// }); +// } + +// /** +// * Get total fonts count across all stores +// */ +// get totalFontsCount() { +// return Array.from(this.stores.values()).reduce( +// (sum, store) => sum + store.fontCount, +// 0 +// ); +// } + +// /** +// * Check if any store is loading +// */ +// get isAnyLoading() { +// return Array.from(this.stores.values()).some(store => store.isLoading); +// } + +// /** +// * Get all errors from all stores +// */ +// get allErrors() { +// return Array.from(this.stores.values()) +// .map(store => store.error) +// .filter((error): error is Error => error !== null); +// } +// } + +// export function createGoogleFontsManager() { +// return new GoogleFontsManager(); +// }