/** * Proxy API client * * Handles API requests to GlyphDiff proxy API for fetching font metadata. * Provides error handling, pagination support, and type-safe responses. * * Proxy API normalizes font data from Google Fonts and Fontshare into a single * unified format, eliminating the need for client-side normalization. * * Fallback: If proxy API fails, falls back to Fontshare API for development. * * @see https://api.glyphdiff.com/api/v1/fonts */ import { api } from '$shared/api/api'; import { buildQueryString } from '$shared/lib/utils'; import type { QueryParams } from '$shared/lib/utils'; import type { UnifiedFont } from '../../model/types'; import type { FontCategory, FontSubset, } from '../../model/types'; /** * Proxy API base URL */ const PROXY_API_URL = 'https://api.glyphdiff.com/api/v1/fonts' as const; /** * Whether to use proxy API (true) or fallback (false) * * Set to true when your proxy API is ready: * const USE_PROXY_API = true; * * Set to false to use Fontshare API as fallback during development: * const USE_PROXY_API = false; * * The app will automatically fall back to Fontshare API if the proxy fails. */ const USE_PROXY_API = true; /** * Proxy API parameters * * Maps directly to the proxy API query parameters */ export interface ProxyFontsParams extends QueryParams { /** * Font provider filter ("google" or "fontshare") * Omit to fetch from both providers */ provider?: 'google' | 'fontshare'; /** * Font category filter */ category?: FontCategory; /** * Character subset filter */ subset?: FontSubset; /** * Search query (e.g., "roboto", "satoshi") */ q?: string; /** * Sort order for results * "name" - Alphabetical by font name * "popularity" - Most popular first * "lastModified" - Recently updated first */ sort?: 'name' | 'popularity' | 'lastModified'; /** * Number of items to return (pagination) */ limit?: number; /** * Number of items to skip (pagination) * Use for pagination: offset = (page - 1) * limit */ offset?: number; } /** * Proxy API response * * Includes pagination metadata alongside font data */ export interface ProxyFontsResponse { /** Array of unified font objects */ fonts: UnifiedFont[]; /** Total number of fonts matching the query */ total: number; /** Limit used for this request */ limit: number; /** Offset used for this request */ offset: number; } /** * Fetch fonts from proxy API * * If proxy API fails or is unavailable, falls back to Fontshare API for development. * * @param params - Query parameters for filtering and pagination * @returns Promise resolving to proxy API response * @throws ApiError when request fails * * @example * ```ts * // Fetch all sans-serif fonts from Google * const response = await fetchProxyFonts({ * provider: 'google', * category: 'sans-serif', * limit: 50, * offset: 0 * }); * * // Search fonts across all providers * const searchResponse = await fetchProxyFonts({ * q: 'roboto', * limit: 20 * }); * * // Fetch fonts with pagination * const page1 = await fetchProxyFonts({ limit: 50, offset: 0 }); * const page2 = await fetchProxyFonts({ limit: 50, offset: 50 }); * ``` */ export async function fetchProxyFonts( params: ProxyFontsParams = {}, ): Promise { // Try proxy API first if enabled if (USE_PROXY_API) { try { const queryString = buildQueryString(params); const url = `${PROXY_API_URL}${queryString}`; console.log('[fetchProxyFonts] Fetching from proxy API', { params, url }); const response = await api.get(url); // Validate response has fonts array if (!response.data || !Array.isArray(response.data.fonts)) { console.error('[fetchProxyFonts] Invalid response from proxy API', response.data); throw new Error('Proxy API returned invalid response'); } console.log('[fetchProxyFonts] Proxy API success', { count: response.data.fonts.length, }); return response.data; } catch (error) { console.warn('[fetchProxyFonts] Proxy API failed, using fallback', error); // Check if it's a network error or proxy not available const isNetworkError = error instanceof Error && (error.message.includes('Failed to fetch') || error.message.includes('Network') || error.message.includes('404') || error.message.includes('500')); if (isNetworkError) { // Fall back to Fontshare API console.log('[fetchProxyFonts] Using Fontshare API as fallback'); return await fetchFontshareFallback(params); } // Re-throw other errors if (error instanceof Error) { throw error; } throw new Error(`Failed to fetch fonts from proxy API: ${String(error)}`); } } // Use Fontshare API directly console.log('[fetchProxyFonts] Using Fontshare API (proxy disabled)'); return await fetchFontshareFallback(params); } /** * Fallback to Fontshare API when proxy is unavailable * * Maps proxy API params to Fontshare API params and normalizes response */ async function fetchFontshareFallback( params: ProxyFontsParams, ): Promise { // Import dynamically to avoid circular dependency const { fetchFontshareFonts } = await import('$entities/Font/api/fontshare/fontshare'); const { normalizeFontshareFonts } = await import('$entities/Font/lib/normalize/normalize'); // Map proxy params to Fontshare params const fontshareParams = { q: params.q, categories: params.category ? [params.category] : undefined, page: params.offset ? Math.floor(params.offset / (params.limit || 50)) + 1 : undefined, limit: params.limit, }; const response = await fetchFontshareFonts(fontshareParams); const normalizedFonts = normalizeFontshareFonts(response.fonts); return { fonts: normalizedFonts, total: response.count_total, limit: params.limit || response.count, offset: params.offset || 0, }; } /** * Fetch font by ID * * Convenience function for fetching a single font by ID * Note: This fetches a page and filters client-side, which is not ideal * For production, consider adding a dedicated endpoint to the proxy API * * @param id - Font ID (family name for Google, slug for Fontshare) * @returns Promise resolving to font or undefined * * @example * ```ts * const roboto = await fetchProxyFontById('Roboto'); * const satoshi = await fetchProxyFontById('satoshi'); * ``` */ export async function fetchProxyFontById( id: string, ): Promise { const response = await fetchProxyFonts({ limit: 1000, q: id }); if (!response || !response.fonts) { console.error('[fetchProxyFontById] No fonts in response', { response }); return undefined; } return response.fonts.find(font => font.id === id); }