feature/fetch-fonts #14
@@ -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 = {}) {
|
||||||
|
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),
|
queryKey: getFontshareQueryKey(params),
|
||||||
queryFn: fetchFontshareFontsQuery,
|
queryFn: () => fetchFontshareFontsQuery(params),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate Fontshare cache
|
* Cancel ongoing queries
|
||||||
* 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(
|
cancel() {
|
||||||
params?: FontshareQueryParams,
|
this.queryClient.cancelQueries({
|
||||||
): void {
|
queryKey: getFontshareQueryKey(this.params),
|
||||||
const queryClient = useQueryClient();
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (params) {
|
/**
|
||||||
queryClient.invalidateQueries({
|
* Clear cache for current params
|
||||||
queryKey: getFontshareQueryKey(params),
|
*/
|
||||||
});
|
clearCache() {
|
||||||
} else {
|
this.queryClient.removeQueries({
|
||||||
queryClient.invalidateQueries({
|
queryKey: getFontshareQueryKey(this.params),
|
||||||
queryKey: ['fontshare'],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = {}) {
|
||||||
* Cancel Fontshare queries
|
return new FontshareStore(params);
|
||||||
* 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'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
|||||||
@@ -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
|
* Google Fonts store wrapping TanStack Query with runes
|
||||||
* 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 = {}) {
|
export class GoogleFontsStore {
|
||||||
useQueryClient();
|
params = $state<GoogleFontsParams>({});
|
||||||
|
private query: CreateQueryResult<UnifiedFont[], Error>;
|
||||||
|
private queryClient = useQueryClient();
|
||||||
|
|
||||||
const query = createQuery(() => ({
|
constructor(initialParams: GoogleFontsParams = {}) {
|
||||||
queryKey: getGoogleFontsQueryKey(params),
|
this.params = initialParams;
|
||||||
queryFn: fetchGoogleFontsQuery,
|
|
||||||
|
// Create the query - automatically reactive
|
||||||
|
this.query = createQuery(() => ({
|
||||||
|
queryKey: getGoogleFontsQueryKey(this.params),
|
||||||
|
queryFn: () => fetchGoogleFontsQuery(this.params),
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
gcTime: 10 * 60 * 1000, // 10 minutes
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return query;
|
// Proxy TanStack Query's reactive state
|
||||||
}
|
get fonts() {
|
||||||
|
return this.query.data ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
get isLoading() {
|
||||||
* Prefetch Google Fonts
|
return this.query.isLoading;
|
||||||
* Fetch fonts in background without showing loading state
|
}
|
||||||
*
|
|
||||||
* @param params - Query parameters for prefetch
|
get isFetching() {
|
||||||
*
|
return this.query.isFetching;
|
||||||
* @example
|
}
|
||||||
* ```ts
|
|
||||||
* // Prefetch fonts when user hovers over button
|
get isRefetching() {
|
||||||
* function onMouseEnter() {
|
return this.query.isRefetching;
|
||||||
* prefetchGoogleFonts({ category: 'sans-serif' });
|
}
|
||||||
* }
|
|
||||||
* ```
|
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(
|
setParams(newParams: Partial<GoogleFontsParams>) {
|
||||||
params: GoogleFontsQueryParams = {},
|
this.params = { ...this.params, ...newParams };
|
||||||
): Promise<void> {
|
}
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
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),
|
queryKey: getGoogleFontsQueryKey(params),
|
||||||
queryFn: fetchGoogleFontsQuery,
|
queryFn: () => fetchGoogleFontsQuery(params),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate Google Fonts cache
|
* Prefetch next category (useful for tab switching)
|
||||||
* 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(
|
async prefetchCategory(category: FontCategory) {
|
||||||
params?: GoogleFontsQueryParams,
|
await this.prefetch({ ...this.params, category });
|
||||||
): void {
|
}
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
if (params) {
|
/**
|
||||||
queryClient.invalidateQueries({
|
* Cancel ongoing queries
|
||||||
queryKey: getGoogleFontsQueryKey(params),
|
*/
|
||||||
|
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
|
* 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();
|
||||||
|
// }
|
||||||
|
|||||||
Reference in New Issue
Block a user