fix: use proper types for fetching fonts

This commit is contained in:
Ilia Mashkov
2026-01-09 16:09:56 +03:00
parent 8ad29fd3a8
commit 6f7e863b13
2 changed files with 569 additions and 335 deletions

View File

@@ -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 { import {
type FontshareParams,
type UnifiedFont,
fetchFontshareFonts,
normalizeFontshareFonts,
} from '$entities/Font';
import {
type CreateQueryResult,
createQuery, createQuery,
useQueryClient, useQueryClient,
} from '@tanstack/svelte-query'; } from '@tanstack/svelte-query';
/** /**
* Fontshare query parameters * Query key factory
*/ */
export interface FontshareQueryParams { function getFontshareQueryKey(params: FontshareParams) {
/** Filter by categories (e.g., ["Sans", "Serif"]) */ return ['fontshare', params] as const;
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;
} }
/** /**
* Query key factory for Fontshare * Query function
* Generates consistent query keys for cache management
*/ */
export function getFontshareQueryKey( async function fetchFontshareFontsQuery(params: FontshareParams): Promise<UnifiedFont[]> {
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;
try { try {
const response = await fetchFontshareFonts({ const response = await fetchFontshareFonts(params);
categories: params.categories, return normalizeFontshareFonts(response.items);
tags: params.tags,
page: params.page,
limit: params.limit,
search: params.search,
});
const normalizedFonts = normalizeFontshareFonts(response.items);
return normalizedFonts;
} catch (error) { } catch (error) {
// User-friendly error messages
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes('Failed to fetch')) { if (error.message.includes('Failed to fetch')) {
throw new Error( 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')) { if (error.message.includes('404')) {
throw new Error('Font not found in Fontshare catalog.'); 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
* <script lang="ts">
* let { categories }: { categories?: string[] } = $props();
*
* const query = useFontshareFontsQuery({ categories });
*
* if ($query.isLoading) {
* return <LoadingSpinner />;
* }
*
* if ($query.error) {
* return <ErrorMessage message={$query.error.message} />;
* }
*
* const fonts = $query.data ?? [];
* </script>
*
* {#each fonts as font}
* <FontCard {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 * Fontshare store wrapping TanStack Query with runes
* 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'] });
* }
* ```
*/ */
export async function prefetchFontshareFonts( export class FontshareStore {
params: FontshareQueryParams = {}, params = $state<FontshareParams>({});
): Promise<void> { private query: CreateQueryResult<UnifiedFont[], Error>;
const queryClient = useQueryClient(); private queryClient = useQueryClient();
await queryClient.prefetchQuery({ constructor(initialParams: FontshareParams = {}) {
queryKey: getFontshareQueryKey(params), this.params = initialParams;
queryFn: fetchFontshareFontsQuery,
staleTime: 5 * 60 * 1000,
});
}
/** // Create the query - it's already reactive
* Invalidate Fontshare cache this.query = createQuery(() => ({
* Forces refetch on next query queryKey: getFontshareQueryKey(this.params),
* queryFn: () => fetchFontshareFontsQuery(this.params),
* @param params - Query parameters to invalidate (all if not provided) staleTime: 5 * 60 * 1000,
* gcTime: 10 * 60 * 1000,
* @example }));
* ```ts }
* // Invalidate all Fontshare cache
* invalidateFontshareFonts();
*
* // Invalidate specific category cache
* invalidateFontshareFonts({ categories: ['Sans'] });
* ```
*/
export function invalidateFontshareFonts(
params?: FontshareQueryParams,
): void {
const queryClient = useQueryClient();
if (params) { // Proxy TanStack Query's reactive state
queryClient.invalidateQueries({ get fonts() {
queryKey: getFontshareQueryKey(params), 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<FontshareParams>) {
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'], 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<UnifiedFont[]>(
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 * Manager for multiple Fontshare stores
* Abort in-flight requests
*
* @param params - Query parameters to cancel (all if not provided)
*
* @example
* ```ts
* // Cancel all Fontshare queries
* cancelFontshareFontsQueries();
* ```
*/ */
export function cancelFontshareFontsQueries( // export class FontshareManager {
params?: FontshareQueryParams, // stores = $state<Map<string, FontshareStore>>(new Map());
): void { // private queryClient = useQueryClient();
const queryClient = useQueryClient();
if (params) { // getStore(params: FontshareParams = {}): FontshareStore {
queryClient.cancelQueries({ // const key = JSON.stringify(params);
queryKey: getFontshareQueryKey(params),
}); // if (!this.stores.has(key)) {
} else { // this.stores.set(key, new FontshareStore(params));
queryClient.cancelQueries({ // }
queryKey: ['fontshare'],
}); // 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();
// }
//

View File

@@ -1,21 +1,16 @@
/** /**
* Service for fetching Google Fonts * Service for fetching Google Fonts with Svelte 5 runes + TanStack Query
*
* Integrates with TanStack Query for caching, deduplication,
* and automatic refetching.
*
* Uses reactive query args pattern for Svelte 5 compatibility.
*/ */
import { fetchGoogleFonts } from '$entities/Font';
import { fetchGoogleFonts } from '$entities/Font/api/google/googleFonts'; import { normalizeGoogleFonts } from '$entities/Font';
import { normalizeGoogleFonts } from '$entities/Font/api/normalize/normalize';
import type { import type {
FontCategory, FontCategory,
FontSubset, FontSubset,
} from '$entities/Font/model/types'; GoogleFontsParams,
import type { UnifiedFont } from '$entities/Font/model/types/normalize'; } from '$entities/Font';
import type { QueryFunction } from '@tanstack/svelte-query'; import type { UnifiedFont } from '$entities/Font';
import { import {
type CreateQueryResult,
createQuery, createQuery,
useQueryClient, useQueryClient,
} from '@tanstack/svelte-query'; } from '@tanstack/svelte-query';
@@ -23,191 +18,372 @@ import {
/** /**
* Google Fonts query parameters * Google Fonts query parameters
*/ */
export interface GoogleFontsQueryParams { // export interface GoogleFontsParams {
/** Font category filter */ // category?: FontCategory;
category?: FontCategory; // subset?: FontSubset;
/** Character subset filter */ // sort?: 'popularity' | 'alpha' | 'date';
subset?: FontSubset; // search?: string;
/** Sort order */ // forceRefetch?: boolean;
sort?: 'popularity' | 'alpha' | 'date'; // }
/** Search query (for specific font) */
search?: string; /**
/** Force refetch even if cached */ * Query key factory
forceRefetch?: boolean; */
function getGoogleFontsQueryKey(params: GoogleFontsParams) {
return ['googleFonts', params] as const;
} }
/** /**
* Query key factory for Google Fonts * Query function
* Generates consistent query keys for cache management
*/ */
export function getGoogleFontsQueryKey( async function fetchGoogleFontsQuery(params: GoogleFontsParams): Promise<UnifiedFont[]> {
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;
try { try {
const response = await fetchGoogleFonts({ const response = await fetchGoogleFonts({
category: params.category, category: params.category,
subset: params.subset, subset: params.subset,
sort: params.sort, sort: params.sort,
}); });
return normalizeGoogleFonts(response.items);
const normalizedFonts = normalizeGoogleFonts(response.items);
return normalizedFonts;
} catch (error) { } catch (error) {
// User-friendly error messages
if (error instanceof Error) { if (error instanceof Error) {
if (error.message.includes('Failed to fetch')) { if (error.message.includes('Failed to fetch')) {
throw new Error( 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')) { if (error.message.includes('404')) {
throw new Error('Font not found in Google Fonts catalog.'); 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
* <script lang="ts">
* let { category }: { category?: FontCategory } = $props();
*
* const query = useGoogleFontsQuery({ category });
*
* if ($query.isLoading) {
* return <LoadingSpinner />;
* }
*
* if ($query.error) {
* return <ErrorMessage message={$query.error.message} />;
* }
*
* const fonts = $query.data ?? [];
* </script>
*
* {#each fonts as font}
* <FontCard {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 * Google Fonts store wrapping TanStack Query with runes
* 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' });
* }
* ```
*/ */
export async function prefetchGoogleFonts( export class GoogleFontsStore {
params: GoogleFontsQueryParams = {}, params = $state<GoogleFontsParams>({});
): Promise<void> { private query: CreateQueryResult<UnifiedFont[], Error>;
const queryClient = useQueryClient(); private queryClient = useQueryClient();
await queryClient.prefetchQuery({ constructor(initialParams: GoogleFontsParams = {}) {
queryKey: getGoogleFontsQueryKey(params), this.params = initialParams;
queryFn: fetchGoogleFontsQuery,
staleTime: 5 * 60 * 1000,
});
}
/** // Create the query - automatically reactive
* Invalidate Google Fonts cache this.query = createQuery(() => ({
* Forces refetch on next query queryKey: getGoogleFontsQueryKey(this.params),
* queryFn: () => fetchGoogleFontsQuery(this.params),
* @param params - Query parameters to invalidate (all if not provided) staleTime: 5 * 60 * 1000, // 5 minutes
* gcTime: 10 * 60 * 1000, // 10 minutes
* @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();
if (params) { // Proxy TanStack Query's reactive state
queryClient.invalidateQueries({ get fonts() {
queryKey: getGoogleFontsQueryKey(params), 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<GoogleFontsParams>) {
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'], 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<UnifiedFont[]>(
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 * Factory function to create Google Fonts store
* Abort in-flight requests
*
* @param params - Query parameters to cancel (all if not provided)
*
* @example
* ```ts
* // Cancel all Google Fonts queries
* cancelGoogleFontsQueries();
* ```
*/ */
export function cancelGoogleFontsQueries( export function createGoogleFontsStore(params: GoogleFontsParams = {}) {
params?: GoogleFontsQueryParams, return new GoogleFontsStore(params);
): void {
const queryClient = useQueryClient();
if (params) {
queryClient.cancelQueries({
queryKey: getGoogleFontsQueryKey(params),
});
} else {
queryClient.cancelQueries({
queryKey: ['googleFonts'],
});
}
} }
/**
* Manager for multiple Google Fonts stores
*/
// export class GoogleFontsManager {
// stores = $state<Map<string, GoogleFontsStore>>(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();
// }