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 {
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<UnifiedFont[]> {
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
* <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
* 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<void> {
const queryClient = useQueryClient();
export class FontshareStore {
params = $state<FontshareParams>({});
private query: CreateQueryResult<UnifiedFont[], Error>;
private queryClient = useQueryClient();
await queryClient.prefetchQuery({
constructor(initialParams: FontshareParams = {}) {
this.params = initialParams;
// 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,
}));
}
// 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<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),
});
}
/**
* 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,
queryFn: () => fetchFontshareFontsQuery(params),
staleTime: 5 * 60 * 1000,
});
}
}
/**
* 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'] });
* ```
/**
* Cancel ongoing queries
*/
export function invalidateFontshareFonts(
params?: FontshareQueryParams,
): void {
const queryClient = useQueryClient();
cancel() {
this.queryClient.cancelQueries({
queryKey: getFontshareQueryKey(this.params),
});
}
if (params) {
queryClient.invalidateQueries({
queryKey: getFontshareQueryKey(params),
});
} else {
queryClient.invalidateQueries({
queryKey: ['fontshare'],
/**
* 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,
);
}
}
/**
* Cancel Fontshare queries
* Abort in-flight requests
*
* @param params - Query parameters to cancel (all if not provided)
*
* @example
* ```ts
* // Cancel all Fontshare queries
* cancelFontshareFontsQueries();
* ```
*/
export function cancelFontshareFontsQueries(
params?: FontshareQueryParams,
): void {
const queryClient = useQueryClient();
if (params) {
queryClient.cancelQueries({
queryKey: getFontshareQueryKey(params),
});
} else {
queryClient.cancelQueries({
queryKey: ['fontshare'],
});
}
export function createFontshareStore(params: FontshareParams = {}) {
return new FontshareStore(params);
}
/**
* Manager for multiple Fontshare stores
*/
// export class FontshareManager {
// stores = $state<Map<string, FontshareStore>>(new Map());
// private queryClient = useQueryClient();
// 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();
// }
//

View File

@@ -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<UnifiedFont[]> {
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
* <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}
* ```
* Google Fonts store wrapping TanStack Query with runes
*/
export function useGoogleFontsQuery(params: GoogleFontsQueryParams = {}) {
useQueryClient();
export class GoogleFontsStore {
params = $state<GoogleFontsParams>({});
private query: CreateQueryResult<UnifiedFont[], Error>;
private queryClient = useQueryClient();
const query = createQuery(() => ({
queryKey: getGoogleFontsQueryKey(params),
queryFn: fetchGoogleFontsQuery,
constructor(initialParams: GoogleFontsParams = {}) {
this.params = initialParams;
// 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
}));
}
return query;
}
// Proxy TanStack Query's reactive state
get fonts() {
return this.query.data ?? [];
}
/**
* 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' });
* }
* ```
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
*/
export async function prefetchGoogleFonts(
params: GoogleFontsQueryParams = {},
): Promise<void> {
const queryClient = useQueryClient();
setParams(newParams: Partial<GoogleFontsParams>) {
this.params = { ...this.params, ...newParams };
}
await queryClient.prefetchQuery({
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),
});
}
/**
* 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,
queryFn: () => fetchGoogleFontsQuery(params),
staleTime: 5 * 60 * 1000,
});
}
}
/**
* 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' });
* ```
/**
* Prefetch next category (useful for tab switching)
*/
export function invalidateGoogleFonts(
params?: GoogleFontsQueryParams,
): void {
const queryClient = useQueryClient();
async prefetchCategory(category: FontCategory) {
await this.prefetch({ ...this.params, category });
}
if (params) {
queryClient.invalidateQueries({
queryKey: getGoogleFontsQueryKey(params),
/**
* Cancel ongoing queries
*/
cancel() {
this.queryClient.cancelQueries({
queryKey: getGoogleFontsQueryKey(this.params),
});
} else {
queryClient.invalidateQueries({
queryKey: ['googleFonts'],
}
/**
* 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
* 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<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();
// }