280 lines
8.1 KiB
TypeScript
280 lines
8.1 KiB
TypeScript
/**
|
|
* 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<ProxyFontsResponse> {
|
|
// 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<ProxyFontsResponse>(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<ProxyFontsResponse> {
|
|
// 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<UnifiedFont | undefined> {
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* Fetch multiple fonts by their IDs
|
|
*
|
|
* @param ids - Array of font IDs to fetch
|
|
* @returns Promise resolving to an array of fonts
|
|
*/
|
|
export async function fetchFontsByIds(ids: string[]): Promise<UnifiedFont[]> {
|
|
if (ids.length === 0) return [];
|
|
|
|
// Use proxy API if enabled
|
|
if (USE_PROXY_API) {
|
|
const queryString = ids.join(',');
|
|
const url = `${PROXY_API_URL}/batch?ids=${queryString}`;
|
|
|
|
try {
|
|
const response = await api.get<UnifiedFont[]>(url);
|
|
return response.data ?? [];
|
|
} catch (error) {
|
|
console.warn('[fetchFontsByIds] Proxy API batch fetch failed, falling back', error);
|
|
// Fallthrough to fallback
|
|
}
|
|
}
|
|
|
|
// Fallback: Fetch individually (not efficient but functional for fallback)
|
|
const results = await Promise.all(
|
|
ids.map(id => fetchProxyFontById(id)),
|
|
);
|
|
|
|
return results.filter((f): f is UnifiedFont => !!f);
|
|
}
|