feat(filters): support multiple values

This commit is contained in:
Ilia Mashkov
2026-03-02 14:12:55 +03:00
parent 37a528f0aa
commit db7ffd3246
3 changed files with 93 additions and 152 deletions

View File

@@ -7,8 +7,6 @@
* Proxy API normalizes font data from Google Fonts and Fontshare into a single * Proxy API normalizes font data from Google Fonts and Fontshare into a single
* unified format, eliminating the need for client-side normalization. * 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 * @see https://api.glyphdiff.com/api/v1/fonts
*/ */
@@ -26,40 +24,37 @@ import type {
*/ */
const PROXY_API_URL = 'https://api.glyphdiff.com/api/v1/fonts' as const; 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 * Proxy API parameters
* *
* Maps directly to the proxy API query parameters * Maps directly to the proxy API query parameters
*
* UPDATED: Now supports array values for filters
*/ */
export interface ProxyFontsParams extends QueryParams { export interface ProxyFontsParams extends QueryParams {
/** /**
* Font provider filter ("google" or "fontshare") * Font provider filter
* Omit to fetch from both providers *
* NEW: Supports array of providers (e.g., ["google", "fontshare"])
* Backward compatible: Single value still works
*/ */
provider?: 'google' | 'fontshare'; providers?: string[] | string;
/** /**
* Font category filter * Font category filter
*
* NEW: Supports array of categories (e.g., ["serif", "sans-serif"])
* Backward compatible: Single value still works
*/ */
category?: FontCategory; categories?: string[] | string;
/** /**
* Character subset filter * Character subset filter
*
* NEW: Supports array of subsets (e.g., ["latin", "cyrillic"])
* Backward compatible: Single value still works
*/ */
subset?: FontSubset; subsets?: string[] | string;
/** /**
* Search query (e.g., "roboto", "satoshi") * Search query (e.g., "roboto", "satoshi")
@@ -108,8 +103,6 @@ export interface ProxyFontsResponse {
/** /**
* Fetch fonts from proxy API * 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 * @param params - Query parameters for filtering and pagination
* @returns Promise resolving to proxy API response * @returns Promise resolving to proxy API response
* @throws ApiError when request fails * @throws ApiError when request fails
@@ -138,84 +131,16 @@ export interface ProxyFontsResponse {
export async function fetchProxyFonts( export async function fetchProxyFonts(
params: ProxyFontsParams = {}, params: ProxyFontsParams = {},
): Promise<ProxyFontsResponse> { ): Promise<ProxyFontsResponse> {
// Try proxy API first if enabled const queryString = buildQueryString(params);
if (USE_PROXY_API) { const url = `${PROXY_API_URL}${queryString}`;
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);
const response = await api.get<ProxyFontsResponse>(url); if (!response.data || !Array.isArray(response.data.fonts)) {
throw new Error('Proxy API returned invalid response');
// 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 return response.data;
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,
};
} }
/** /**
@@ -256,24 +181,9 @@ export async function fetchProxyFontById(
export async function fetchFontsByIds(ids: string[]): Promise<UnifiedFont[]> { export async function fetchFontsByIds(ids: string[]): Promise<UnifiedFont[]> {
if (ids.length === 0) return []; if (ids.length === 0) return [];
// Use proxy API if enabled const queryString = ids.join(',');
if (USE_PROXY_API) { const url = `${PROXY_API_URL}/batch?ids=${queryString}`;
const queryString = ids.join(',');
const url = `${PROXY_API_URL}/batch?ids=${queryString}`;
try { const response = await api.get<UnifiedFont[]>(url);
const response = await api.get<UnifiedFont[]>(url); return response.data ?? [];
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);
} }

View File

@@ -4,8 +4,7 @@ import type { FilterManager } from '../filterManager/filterManager.svelte';
/** /**
* Maps filter manager to proxy API parameters. * Maps filter manager to proxy API parameters.
* *
* Transforms UI filter state into proxy API query parameters. * Updated to support multiple filter values (arrays)
* Handles conversion from filter groups to API-specific parameters.
* *
* @param manager - Filter manager instance with reactive state * @param manager - Filter manager instance with reactive state
* @returns - Partial proxy API parameters ready for API call * @returns - Partial proxy API parameters ready for API call
@@ -15,13 +14,18 @@ import type { FilterManager } from '../filterManager/filterManager.svelte';
* // Example filter manager state: * // Example filter manager state:
* // { * // {
* // queryValue: 'roboto', * // queryValue: 'roboto',
* // providers: ['google'], * // providers: ['google', 'fontshare'],
* // categories: ['sans-serif'], * // categories: ['sans-serif', 'serif'],
* // subsets: ['latin'] * // subsets: ['latin']
* // } * // }
* *
* const params = mapManagerToParams(manager); * const params = mapManagerToParams(manager);
* // Returns: { provider: 'google', category: 'sans-serif', subset: 'latin', q: 'roboto' } * // Returns: {
* // providers: ['google', 'fontshare'],
* // categories: ['sans-serif', 'serif'],
* // subsets: ['latin'],
* // q: 'roboto'
* // }
* ``` * ```
*/ */
export function mapManagerToParams(manager: FilterManager): Partial<ProxyFontsParams> { export function mapManagerToParams(manager: FilterManager): Partial<ProxyFontsParams> {
@@ -33,22 +37,17 @@ export function mapManagerToParams(manager: FilterManager): Partial<ProxyFontsPa
// Search query (debounced) // Search query (debounced)
q: manager.debouncedQueryValue || undefined, q: manager.debouncedQueryValue || undefined,
// Provider filter (single value - proxy API doesn't support array) // NEW: Support arrays - send all selected values
// Use first provider if multiple selected, or undefined if none/all selected providers: providers && providers.length > 0
provider: providers && providers.length === 1 ? providers as string[]
? (providers[0] as 'google' | 'fontshare')
: undefined, : undefined,
// Category filter (single value - proxy API doesn't support array) categories: categories && categories.length > 0
// Use first category if multiple selected, or undefined if none/all selected ? categories as string[]
category: categories && categories.length === 1
? (categories[0] as ProxyFontsParams['category'])
: undefined, : undefined,
// Subset filter (single value - proxy API doesn't support array) subsets: subsets && subsets.length > 0
// Use first subset if multiple selected, or undefined if none/all selected ? subsets as string[]
subset: subsets && subsets.length === 1
? (subsets[0] as ProxyFontsParams['subset'])
: undefined, : undefined,
}; };
} }

View File

@@ -4,26 +4,58 @@ import {
FONT_PROVIDERS, FONT_PROVIDERS,
FONT_SUBSETS, FONT_SUBSETS,
} from '../const/const'; } from '../const/const';
import { filtersStore } from './filters.svelte';
const initialConfig = { /**
queryValue: '', * Creates initial filter config
groups: [ *
{ * Uses dynamic filters from backend if available,
id: 'providers', * otherwise falls back to hard-coded constants
label: 'Font provider', */
properties: FONT_PROVIDERS, function createInitialConfig() {
}, const dynamicFilters = filtersStore.filters;
{
id: 'subsets', // If filters are loaded, use them
label: 'Font subset', if (dynamicFilters.length > 0) {
properties: FONT_SUBSETS, return {
}, queryValue: '',
{ groups: dynamicFilters.map(filter => ({
id: 'categories', id: filter.id,
label: 'Font category', label: filter.name,
properties: FONT_CATEGORIES, properties: filter.options.map(opt => ({
}, id: opt.id,
], name: opt.name,
}; value: opt.value,
count: opt.count,
selected: false,
})),
})),
};
}
// Fallback to hard-coded constants (backward compatibility)
return {
queryValue: '',
groups: [
{
id: 'providers',
label: 'Font provider',
properties: FONT_PROVIDERS,
},
{
id: 'subsets',
label: 'Font subset',
properties: FONT_SUBSETS,
},
{
id: 'categories',
label: 'Font category',
properties: FONT_CATEGORIES,
},
],
};
}
const initialConfig = createInitialConfig();
export const filterManager = createFilterManager(initialConfig); export const filterManager = createFilterManager(initialConfig);