refactor(Font): consolidate API layer and update type structure

This commit is contained in:
Ilia Mashkov
2026-03-02 22:18:21 +03:00
parent ba186d00a1
commit af4137f47f
17 changed files with 325 additions and 558 deletions
@@ -7,41 +7,64 @@ import {
} from '@tanstack/query-core';
import type { UnifiedFont } from '../types';
/** */
/**
* Base class for font stores using TanStack Query
*
* Provides reactive font data fetching with caching, automatic refetching,
* and parameter binding. Extended by UnifiedFontStore for provider-agnostic
* font fetching.
*
* @template TParams - Type of query parameters
*/
export abstract class BaseFontStore<TParams extends Record<string, any>> {
/**
* Cleanup function for effects
* Call destroy() to remove effects and prevent memory leaks
*/
cleanup: () => void;
/** Reactive parameter bindings from external sources */
#bindings = $state<(() => Partial<TParams>)[]>([]);
/** Internal parameter state */
#internalParams = $state<TParams>({} as TParams);
/**
* Merged params from internal state and all bindings
* Automatically updates when bindings or internal params change
*/
params = $derived.by(() => {
let merged = { ...this.#internalParams };
// Loop through every "Cable" plugged into the store
// Loop through every "Cable" plugged into the store
// Merge all binding results into params
for (const getter of this.#bindings) {
const bindingResult = getter();
merged = { ...merged, ...bindingResult };
}
return merged as TParams;
});
/** TanStack Query result state */
protected result = $state<QueryObserverResult<UnifiedFont[], Error>>({} as any);
/** TanStack Query observer instance */
protected observer: QueryObserver<UnifiedFont[], Error>;
/** Shared query client */
protected qc = queryClient;
/**
* Creates a new base font store
* @param initialParams - Initial query parameters
*/
constructor(initialParams: TParams) {
this.#internalParams = initialParams;
this.observer = new QueryObserver(this.qc, this.getOptions());
// Sync TanStack -> Svelte State
// Sync TanStack Query state -> Svelte state
this.observer.subscribe(r => {
this.result = r;
});
// Sync Svelte State -> TanStack Options
// Sync Svelte state changes -> TanStack Query options
this.cleanup = $effect.root(() => {
$effect(() => {
this.observer.setOptions(this.getOptions());
@@ -50,11 +73,21 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
}
/**
* Mandatory: Child must define how to fetch data and what the key is.
* Must be implemented by child class
* Returns the query key for TanStack Query caching
*/
protected abstract getQueryKey(params: TParams): QueryKey;
/**
* Must be implemented by child class
* Fetches font data from API
*/
protected abstract fetchFn(params: TParams): Promise<UnifiedFont[]>;
/**
* Gets TanStack Query options
* @param params - Query parameters (defaults to current params)
*/
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
return {
queryKey: this.getQueryKey(params),
@@ -64,25 +97,36 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
};
}
// --- Common Getters ---
/** Array of fonts (empty array if loading/error) */
get fonts() {
return this.result.data ?? [];
}
/** Whether currently fetching initial data */
get isLoading() {
return this.result.isLoading;
}
/** Whether any fetch is in progress (including refetches) */
get isFetching() {
return this.result.isFetching;
}
/** Whether last fetch resulted in an error */
get isError() {
return this.result.isError;
}
/** Whether no fonts are loaded (not loading and empty array) */
get isEmpty() {
return !this.isLoading && this.fonts.length === 0;
}
// --- Common Actions ---
/**
* Add a reactive parameter binding
* @param getter - Function that returns partial params to merge
* @returns Unbind function to remove the binding
*/
addBinding(getter: () => Partial<TParams>) {
this.#bindings.push(getter);
@@ -91,9 +135,14 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
};
}
/**
* Update query parameters
* @param newParams - Partial params to merge with existing
*/
setParams(newParams: Partial<TParams>) {
this.#internalParams = { ...this.params, ...newParams };
}
/**
* Invalidate cache and refetch
*/
@@ -101,19 +150,22 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
this.qc.invalidateQueries({ queryKey: this.getQueryKey(this.params) });
}
/**
* Clean up effects and observers
*/
destroy() {
this.cleanup();
}
/**
* Manually refetch
* Manually trigger a refetch
*/
async refetch() {
await this.observer.refetch();
}
/**
* Prefetch with different params (for hover states, pagination, etc.)
* Prefetch data with different parameters
*/
async prefetch(params: TParams) {
await this.qc.prefetchQuery(this.getOptions(params));
-43
View File
@@ -1,43 +0,0 @@
/**
* ============================================================================
* UNIFIED FONT STORE TYPES
* ============================================================================
*
* Type definitions for the unified font store infrastructure.
* Provides types for filters, sorting, and fetch parameters.
*/
import type {
FontshareParams,
GoogleFontsParams,
} from '$entities/Font/api';
import type {
FontCategory,
FontProvider,
FontSubset,
} from '$entities/Font/model/types/common';
/**
* Sort configuration
*/
export interface FontSort {
field: 'name' | 'popularity' | 'category' | 'date';
direction: 'asc' | 'desc';
}
/**
* Fetch params for unified API
*/
export interface FetchFontsParams {
providers?: FontProvider[];
categories?: FontCategory[];
subsets?: FontSubset[];
search?: string;
sort?: FontSort;
forceRefetch?: boolean;
}
/**
* Provider-specific params union
*/
export type ProviderParams = GoogleFontsParams | FontshareParams;
@@ -43,7 +43,7 @@ import { BaseFontStore } from './baseFontStore.svelte';
* });
*
* // Update parameters
* store.setCategory('serif');
* store.setCategories(['serif']);
* store.nextPage();
* ```
*/
@@ -108,16 +108,20 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
this.#filterCleanup = $effect.root(() => {
$effect(() => {
const filterParams = JSON.stringify({
provider: this.params.provider,
category: this.params.category,
subset: this.params.subset,
providers: this.params.providers,
categories: this.params.categories,
subsets: this.params.subsets,
q: this.params.q,
});
// If filters changed, reset offset to 0
// If filters changed, reset offset and invalidate cache
if (filterParams !== this.#previousFilterParams) {
if (this.#previousFilterParams && this.params.offset !== 0) {
this.setParams({ offset: 0 });
if (this.#previousFilterParams) {
if (this.params.offset !== 0) {
this.setParams({ offset: 0 });
}
this.#accumulatedFonts = [];
this.invalidate();
}
this.#previousFilterParams = filterParams;
}
@@ -170,7 +174,7 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
}
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
const hasFilters = !!(params.q || params.provider || params.category || params.subset);
const hasFilters = !!(params.q || params.providers || params.categories || params.subsets);
return {
queryKey: this.getQueryKey(params),
queryFn: () => this.fetchFn(params),
@@ -221,8 +225,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
return response.fonts;
}
// --- Getters (proxied from BaseFontStore) ---
/**
* Get all accumulated fonts (for infinite scroll)
*/
@@ -258,27 +260,25 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
return !this.isLoading && this.fonts.length === 0;
}
// --- Provider-specific shortcuts ---
/**
* Set provider filter
* Set providers filter
*/
setProvider(provider: 'google' | 'fontshare' | undefined) {
this.setParams({ provider });
setProviders(providers: ProxyFontsParams['providers']) {
this.setParams({ providers });
}
/**
* Set category filter
* Set categories filter
*/
setCategory(category: ProxyFontsParams['category']) {
this.setParams({ category });
setCategories(categories: ProxyFontsParams['categories']) {
this.setParams({ categories });
}
/**
* Set subset filter
* Set subsets filter
*/
setSubset(subset: ProxyFontsParams['subset']) {
this.setParams({ subset });
setSubsets(subsets: ProxyFontsParams['subsets']) {
this.setParams({ subsets });
}
/**
@@ -295,8 +295,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
this.setParams({ sort });
}
// --- Pagination methods ---
/**
* Go to next page
*/
@@ -337,8 +335,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
this.setParams({ limit });
}
// --- Category shortcuts (for convenience) ---
get sansSerifFonts() {
return this.fonts.filter(f => f.category === 'sans-serif');
}
+20 -10
View File
@@ -1,50 +1,60 @@
/**
* ============================================================================
* DOMAIN TYPES
* ============================================================================
* Common font domain types
*
* Shared types for font entities across providers (Google, Fontshare).
* Includes categories, subsets, weights, and filter types.
*/
import type { FontCategory as FontshareFontCategory } from './fontshare';
import type { FontCategory as GoogleFontCategory } from './google';
/**
* Font category
* Unified font category across all providers
*/
export type FontCategory = GoogleFontCategory | FontshareFontCategory;
/**
* Font provider
* Font provider identifier
*/
export type FontProvider = 'google' | 'fontshare';
/**
* Font subset
* Character subset support
*/
export type FontSubset = 'latin' | 'latin-ext' | 'cyrillic' | 'greek' | 'arabic' | 'devanagari';
/**
* Filter state
* Combined filter state for font queries
*/
export interface FontFilters {
/** Selected font providers */
providers: FontProvider[];
/** Selected font categories */
categories: FontCategory[];
/** Selected character subsets */
subsets: FontSubset[];
}
/** Filter group identifier */
export type FilterGroup = 'providers' | 'categories' | 'subsets';
/** Filter type including search query */
export type FilterType = FilterGroup | 'searchQuery';
/**
* Standard font weights
* Numeric font weights (100-900)
*/
export type FontWeight = '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
/**
* Italic variant format: e.g., "100italic", "400italic", "700italic"
* Italic variant with weight: "100italic", "400italic", "700italic", etc.
*/
export type FontWeightItalic = `${FontWeight}italic`;
/**
* All possible font variants
* All possible font variant identifiers
*
* Includes:
* - Numeric weights: "400", "700", etc.
* - Italic variants: "400italic", "700italic", etc.
* - Legacy names: "regular", "italic", "bold", "bolditalic"
@@ -46,6 +46,13 @@ export interface FontMetadata {
lastModified?: string;
/** Popularity rank (if available from provider) */
popularity?: number;
/**
* Normalized popularity score (0-100)
*
* Normalized across all fonts for consistent ranking
* Higher values indicate more popular fonts
*/
popularityScore?: number;
}
/**
@@ -79,6 +86,13 @@ export interface UnifiedFont {
name: string;
/** Font provider (google | fontshare) */
provider: FontProvider;
/**
* Provider badge display name
*
* Human-readable provider name for UI display
* e.g., "Google Fonts" or "Fontshare"
*/
providerBadge?: string;
/** Font category classification */
category: FontCategory;
/** Supported character subsets */