From 7fbeef68e2ff2592584d9b6875203fea043fcd53 Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Thu, 29 Jan 2026 14:38:07 +0300 Subject: [PATCH] feat(fonts): implement Phase 2 - Unified Font Store - Implemented UnifiedFontStore extending BaseFontStore - Added pagination support with derived metadata - Added provider-specific shortcuts (setProvider, setCategory, etc.) - Added pagination methods (nextPage, prevPage, goToPage) - Added category getter shortcuts (sansSerifFonts, serifFonts, etc.) - Updated store exports to include unified store - Fixed typo in googleFontsStore.svelte.ts (createGoogleFontsStore) Phase 2/7: Proxy API Integration for GlyphDiff --- .../model/store/googleFontsStore.svelte.ts | 2 +- src/entities/Font/model/store/index.ts | 27 +- .../model/store/unifiedFontStore.svelte.ts | 284 ++++++++++++++++-- 3 files changed, 287 insertions(+), 26 deletions(-) diff --git a/src/entities/Font/model/store/googleFontsStore.svelte.ts b/src/entities/Font/model/store/googleFontsStore.svelte.ts index 428c476..8fb7ef4 100644 --- a/src/entities/Font/model/store/googleFontsStore.svelte.ts +++ b/src/entities/Font/model/store/googleFontsStore.svelte.ts @@ -20,7 +20,7 @@ export class GoogleFontsStore extends BaseFontStore { } } -export function createFontshareStore(params: GoogleFontsParams = {}) { +export function createGoogleFontsStore(params: GoogleFontsParams = {}) { return new GoogleFontsStore(params); } diff --git a/src/entities/Font/model/store/index.ts b/src/entities/Font/model/store/index.ts index f019a3c..18aba54 100644 --- a/src/entities/Font/model/store/index.ts +++ b/src/entities/Font/model/store/index.ts @@ -6,18 +6,29 @@ * Single export point for the unified font store infrastructure. */ -// export { -// createUnifiedFontStore, -// UNIFIED_FONT_STORE_KEY, -// type UnifiedFontStore, -// } from './unifiedFontStore.svelte'; +// Primary store (unified) +export { + createUnifiedFontStore, + type UnifiedFontStore, + unifiedFontStore, +} from './unifiedFontStore.svelte'; +// Applied fonts manager (CSS loading - unchanged) +export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte'; + +// Selected fonts store (user selection - unchanged) +export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte'; + +// DEPRECATED: Fontshare store (will be removed in Phase 6) export { createFontshareStore, type FontshareStore, fontshareStore, } from './fontshareStore.svelte'; -export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte'; - -export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte'; +// DEPRECATED: Google Fonts store (will be removed in Phase 6) +export { + createGoogleFontsStore, + type GoogleFontsStore, + googleFontsStore, +} from './googleFontsStore.svelte'; diff --git a/src/entities/Font/model/store/unifiedFontStore.svelte.ts b/src/entities/Font/model/store/unifiedFontStore.svelte.ts index 460e521..59dabc0 100644 --- a/src/entities/Font/model/store/unifiedFontStore.svelte.ts +++ b/src/entities/Font/model/store/unifiedFontStore.svelte.ts @@ -1,25 +1,275 @@ -import { type Filter } from '$shared/lib'; -import { SvelteMap } from 'svelte/reactivity'; -import type { FontProvider } from '../types'; -import type { CheckboxFilter } from '../types/common'; -import type { BaseFontStore } from './baseFontStore.svelte'; -import { createFontshareStore } from './fontshareStore.svelte'; -import type { ProviderParams } from './types'; +/** + * Unified font store + * + * Single source of truth for font data, powered by the proxy API. + * Extends BaseFontStore for TanStack Query integration and reactivity. + * + * Key features: + * - Provider-agnostic (proxy API handles provider logic) + * - Reactive to filter changes + * - Optimistic updates via TanStack Query + * - Pagination support + * - Provider-specific shortcuts for common operations + */ -export class UnitedFontStore { - private sources: Partial>>; +import type { ProxyFontsParams } from '../../api'; +import { fetchProxyFonts } from '../../api'; +import type { UnifiedFont } from '../types'; +import type { FontCategory } from '../types'; +import { BaseFontStore } from './baseFontStore.svelte'; - filters: SvelteMap; - queryValue = $state(''); +/** + * Unified font store wrapping TanStack Query with Svelte 5 runes + * + * Extends BaseFontStore to provide: + * - Reactive state management + * - TanStack Query integration for caching + * - Dynamic parameter binding for filters + * - Pagination support + * + * @example + * ```ts + * const store = new UnifiedFontStore({ + * provider: 'google', + * category: 'sans-serif', + * limit: 50 + * }); + * + * // Access reactive state + * $effect(() => { + * console.log(store.fonts); + * console.log(store.isLoading); + * console.log(store.pagination); + * }); + * + * // Update parameters + * store.setCategory('serif'); + * store.nextPage(); + * ``` + */ +export class UnifiedFontStore extends BaseFontStore { + /** + * Store pagination metadata separately from fonts + * This is a workaround for TanStack Query's type system + */ + #paginationMetadata = $state< + { + total: number; + limit: number; + offset: number; + } | null + >(null); - constructor(initialConfig: Partial> = {}) { - this.sources = { - fontshare: createFontshareStore(initialConfig?.fontshare), + /** + * Pagination metadata (derived from proxy API response) + */ + readonly pagination = $derived.by(() => { + if (this.#paginationMetadata) { + const { total, limit, offset } = this.#paginationMetadata; + return { + total, + limit, + offset, + hasMore: offset + limit < total, + page: Math.floor(offset / limit) + 1, + totalPages: Math.ceil(total / limit), + }; + } + return { + total: 0, + limit: this.params.limit || 50, + offset: this.params.offset || 0, + hasMore: false, + page: 1, + totalPages: 0, }; - this.filters = new SvelteMap(); + }); + + constructor(initialParams: ProxyFontsParams = {}) { + super(initialParams); } - get fonts() { - return Object.values(this.sources).map(store => store.fonts).flat(); + /** + * Query key for TanStack Query caching + * Normalizes params to treat empty arrays/strings as undefined + */ + protected getQueryKey(params: ProxyFontsParams) { + // Normalize params to treat empty arrays/strings as undefined + const normalized = Object.entries(params).reduce((acc, [key, value]) => { + if (value === '' || (Array.isArray(value) && value.length === 0)) { + return acc; + } + return { ...acc, [key]: value }; + }, {}); + + return ['unifiedFonts', normalized] as const; + } + + /** + * Fetch function that calls the proxy API + * Returns the full response including pagination metadata + */ + protected async fetchFn(params: ProxyFontsParams): Promise { + const response = await fetchProxyFonts(params); + + // Store pagination metadata separately for derived values + this.#paginationMetadata = { + total: response.total, + limit: response.limit, + offset: response.offset, + }; + + return response.fonts; + } + + // --- Getters (proxied from BaseFontStore) --- + + /** + * Get all fonts from current query result + */ + get fonts(): UnifiedFont[] { + // The result.data is UnifiedFont[] (from TanStack Query) + return (this.result.data as UnifiedFont[] | undefined) ?? []; + } + + /** + * Check if loading initial data + */ + get isLoading(): boolean { + return this.result.isLoading; + } + + /** + * Check if fetching (including background refetches) + */ + get isFetching(): boolean { + return this.result.isFetching; + } + + /** + * Check if error occurred + */ + get isError(): boolean { + return this.result.isError; + } + + /** + * Check if result is empty (not loading and no fonts) + */ + get isEmpty(): boolean { + return !this.isLoading && this.fonts.length === 0; + } + + // --- Provider-specific shortcuts --- + + /** + * Set provider filter + */ + setProvider(provider: 'google' | 'fontshare' | undefined) { + this.setParams({ provider }); + } + + /** + * Set category filter + */ + setCategory(category: ProxyFontsParams['category']) { + this.setParams({ category }); + } + + /** + * Set subset filter + */ + setSubset(subset: ProxyFontsParams['subset']) { + this.setParams({ subset }); + } + + /** + * Set search query + */ + setSearch(search: string) { + this.setParams({ q: search || undefined }); + } + + /** + * Set sort order + */ + setSort(sort: ProxyFontsParams['sort']) { + this.setParams({ sort }); + } + + // --- Pagination methods --- + + /** + * Go to next page + */ + nextPage() { + if (this.pagination.hasMore) { + this.setParams({ + offset: this.pagination.offset + this.pagination.limit, + }); + } + } + + /** + * Go to previous page + */ + prevPage() { + if (this.pagination.page > 1) { + this.setParams({ + offset: this.pagination.offset - this.pagination.limit, + }); + } + } + + /** + * Go to specific page + */ + goToPage(page: number) { + if (page >= 1 && page <= this.pagination.totalPages) { + this.setParams({ + offset: (page - 1) * this.pagination.limit, + }); + } + } + + /** + * Set limit (items per page) + */ + setLimit(limit: number) { + this.setParams({ limit }); + } + + // --- Category shortcuts (for convenience) --- + + 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'); } } + +/** + * Factory function to create unified font store + */ +export function createUnifiedFontStore(params: ProxyFontsParams = {}) { + return new UnifiedFontStore(params); +} + +/** + * Singleton instance for global use + */ +export const unifiedFontStore = new UnifiedFontStore();