refactor(Font): consolidate API layer and update type structure
This commit is contained in:
@@ -1,161 +0,0 @@
|
|||||||
/**
|
|
||||||
* Fontshare API client
|
|
||||||
*
|
|
||||||
* Handles API requests to Fontshare API for fetching font metadata.
|
|
||||||
* Provides error handling, pagination support, and type-safe responses.
|
|
||||||
*
|
|
||||||
* Pagination: The Fontshare API DOES support pagination via `page` and `limit` parameters.
|
|
||||||
* However, the current implementation uses `fetchAllFontshareFonts()` to fetch all fonts upfront.
|
|
||||||
* For future optimization, consider implementing incremental pagination for large datasets.
|
|
||||||
*
|
|
||||||
* @see https://fontshare.com
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { api } from '$shared/api/api';
|
|
||||||
import { buildQueryString } from '$shared/lib/utils';
|
|
||||||
import type { QueryParams } from '$shared/lib/utils';
|
|
||||||
import type {
|
|
||||||
FontshareApiModel,
|
|
||||||
FontshareFont,
|
|
||||||
} from '../../model/types/fontshare';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fontshare API parameters
|
|
||||||
*/
|
|
||||||
export interface FontshareParams extends QueryParams {
|
|
||||||
/**
|
|
||||||
* Filter by categories (e.g., ["Sans", "Serif", "Display"])
|
|
||||||
*/
|
|
||||||
categories?: string[];
|
|
||||||
/**
|
|
||||||
* Filter by tags (e.g., ["Magazines", "Branding", "Logos"])
|
|
||||||
*/
|
|
||||||
tags?: string[];
|
|
||||||
/**
|
|
||||||
* Page number for pagination (1-indexed)
|
|
||||||
*/
|
|
||||||
page?: number;
|
|
||||||
/**
|
|
||||||
* Number of items per page
|
|
||||||
*/
|
|
||||||
limit?: number;
|
|
||||||
/**
|
|
||||||
* Search query to filter fonts
|
|
||||||
*/
|
|
||||||
q?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fontshare API response wrapper
|
|
||||||
* Re-exported from model/types/fontshare for backward compatibility
|
|
||||||
*/
|
|
||||||
export type FontshareResponse = FontshareApiModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch fonts from Fontshare API
|
|
||||||
*
|
|
||||||
* @param params - Query parameters for filtering fonts
|
|
||||||
* @returns Promise resolving to Fontshare API response
|
|
||||||
* @throws ApiError when request fails
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* // Fetch all Sans category fonts
|
|
||||||
* const response = await fetchFontshareFonts({
|
|
||||||
* categories: ['Sans'],
|
|
||||||
* limit: 50
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Fetch fonts with specific tags
|
|
||||||
* const response = await fetchFontshareFonts({
|
|
||||||
* tags: ['Branding', 'Logos']
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Search fonts
|
|
||||||
* const response = await fetchFontshareFonts({
|
|
||||||
* search: 'Satoshi'
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export async function fetchFontshareFonts(
|
|
||||||
params: FontshareParams = {},
|
|
||||||
): Promise<FontshareResponse> {
|
|
||||||
const queryString = buildQueryString(params);
|
|
||||||
const url = `https://api.fontshare.com/v2/fonts${queryString}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await api.get<FontshareResponse>(url);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
// Re-throw ApiError with context
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to fetch Fontshare fonts: ${String(error)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch font by slug
|
|
||||||
* Convenience function for fetching a single font
|
|
||||||
*
|
|
||||||
* @param slug - Font slug (e.g., "satoshi", "general-sans")
|
|
||||||
* @returns Promise resolving to Fontshare font item
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const satoshi = await fetchFontshareFontBySlug('satoshi');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export async function fetchFontshareFontBySlug(
|
|
||||||
slug: string,
|
|
||||||
): Promise<FontshareFont | undefined> {
|
|
||||||
const response = await fetchFontshareFonts();
|
|
||||||
return response.fonts.find(font => font.slug === slug);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all fonts from Fontshare
|
|
||||||
* Convenience function for fetching all available fonts
|
|
||||||
* Uses pagination to get all items
|
|
||||||
*
|
|
||||||
* @returns Promise resolving to all Fontshare fonts
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const allFonts = await fetchAllFontshareFonts();
|
|
||||||
* console.log(`Found ${allFonts.fonts.length} fonts`);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export async function fetchAllFontshareFonts(
|
|
||||||
params: FontshareParams = {},
|
|
||||||
): Promise<FontshareResponse> {
|
|
||||||
const allFonts: FontshareFont[] = [];
|
|
||||||
let page = 1;
|
|
||||||
const limit = 100; // Max items per page
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const response = await fetchFontshareFonts({
|
|
||||||
...params,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
allFonts.push(...response.fonts);
|
|
||||||
|
|
||||||
// Check if we've fetched all items
|
|
||||||
if (response.fonts.length < limit) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
page++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return first response with all items combined
|
|
||||||
const firstResponse = await fetchFontshareFonts({ ...params, page: 1, limit });
|
|
||||||
|
|
||||||
return {
|
|
||||||
...firstResponse,
|
|
||||||
fonts: allFonts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
/**
|
|
||||||
* Google Fonts API client
|
|
||||||
*
|
|
||||||
* Handles API requests to Google Fonts API for fetching font metadata.
|
|
||||||
* Provides error handling, retry logic, and type-safe responses.
|
|
||||||
*
|
|
||||||
* Pagination: The Google Fonts API does NOT support pagination parameters.
|
|
||||||
* All fonts matching the query are returned in a single response.
|
|
||||||
* Use category, subset, or sort filters to reduce the result set if needed.
|
|
||||||
*
|
|
||||||
* @see https://developers.google.com/fonts/docs/developer_api
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { api } from '$shared/api/api';
|
|
||||||
import { buildQueryString } from '$shared/lib/utils';
|
|
||||||
import type { QueryParams } from '$shared/lib/utils';
|
|
||||||
import type {
|
|
||||||
FontItem,
|
|
||||||
GoogleFontsApiModel,
|
|
||||||
} from '../../model/types/google';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Fonts API parameters
|
|
||||||
*/
|
|
||||||
export interface GoogleFontsParams extends QueryParams {
|
|
||||||
/**
|
|
||||||
* Google Fonts API key (required for Google Fonts API v1)
|
|
||||||
*/
|
|
||||||
key?: string;
|
|
||||||
/**
|
|
||||||
* Font family name (to fetch specific font)
|
|
||||||
*/
|
|
||||||
family?: string;
|
|
||||||
/**
|
|
||||||
* Font category filter (e.g., "sans-serif", "serif", "display")
|
|
||||||
*/
|
|
||||||
category?: string;
|
|
||||||
/**
|
|
||||||
* Character subset filter (e.g., "latin", "latin-ext", "cyrillic")
|
|
||||||
*/
|
|
||||||
subset?: string;
|
|
||||||
/**
|
|
||||||
* Sort order for results
|
|
||||||
*/
|
|
||||||
sort?: 'alpha' | 'date' | 'popularity' | 'style' | 'trending';
|
|
||||||
/**
|
|
||||||
* Cap the number of fonts returned
|
|
||||||
*/
|
|
||||||
capability?: 'VF' | 'WOFF2';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Fonts API response wrapper
|
|
||||||
* Re-exported from model/types/google for backward compatibility
|
|
||||||
*/
|
|
||||||
export type GoogleFontsResponse = GoogleFontsApiModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simplified font item from Google Fonts API
|
|
||||||
* Re-exported from model/types/google for backward compatibility
|
|
||||||
*/
|
|
||||||
export type GoogleFontItem = FontItem;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Fonts API base URL
|
|
||||||
* Note: Google Fonts API v1 requires an API key. For development/testing without a key,
|
|
||||||
* fonts may not load properly.
|
|
||||||
*/
|
|
||||||
const GOOGLE_FONTS_API_URL = 'https://www.googleapis.com/webfonts/v1/webfonts' as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch fonts from Google Fonts API
|
|
||||||
*
|
|
||||||
* @param params - Query parameters for filtering fonts
|
|
||||||
* @returns Promise resolving to Google Fonts API response
|
|
||||||
* @throws ApiError when request fails
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* // Fetch all sans-serif fonts sorted by popularity
|
|
||||||
* const response = await fetchGoogleFonts({
|
|
||||||
* category: 'sans-serif',
|
|
||||||
* sort: 'popularity'
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Fetch specific font family
|
|
||||||
* const robotoResponse = await fetchGoogleFonts({
|
|
||||||
* family: 'Roboto'
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export async function fetchGoogleFonts(
|
|
||||||
params: GoogleFontsParams = {},
|
|
||||||
): Promise<GoogleFontsResponse> {
|
|
||||||
const queryString = buildQueryString(params);
|
|
||||||
const url = `${GOOGLE_FONTS_API_URL}${queryString}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await api.get<GoogleFontsResponse>(url);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
// Re-throw ApiError with context
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to fetch Google Fonts: ${String(error)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch font by family name
|
|
||||||
* Convenience function for fetching a single font
|
|
||||||
*
|
|
||||||
* @param family - Font family name (e.g., "Roboto")
|
|
||||||
* @returns Promise resolving to Google Font item
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* const roboto = await fetchGoogleFontFamily('Roboto');
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export async function fetchGoogleFontFamily(
|
|
||||||
family: string,
|
|
||||||
): Promise<GoogleFontItem | undefined> {
|
|
||||||
const response = await fetchGoogleFonts({ family });
|
|
||||||
return response.items.find(item => item.family === family);
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* Exports API clients and normalization utilities
|
* Exports API clients and normalization utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Proxy API (PRIMARY - NEW)
|
// Proxy API (primary)
|
||||||
export {
|
export {
|
||||||
fetchFontsByIds,
|
fetchFontsByIds,
|
||||||
fetchProxyFontById,
|
fetchProxyFontById,
|
||||||
@@ -14,25 +14,3 @@ export type {
|
|||||||
ProxyFontsParams,
|
ProxyFontsParams,
|
||||||
ProxyFontsResponse,
|
ProxyFontsResponse,
|
||||||
} from './proxy/proxyFonts';
|
} from './proxy/proxyFonts';
|
||||||
|
|
||||||
// Google Fonts API (DEPRECATED - kept for backward compatibility)
|
|
||||||
export {
|
|
||||||
fetchGoogleFontFamily,
|
|
||||||
fetchGoogleFonts,
|
|
||||||
} from './google/googleFonts';
|
|
||||||
export type {
|
|
||||||
GoogleFontItem,
|
|
||||||
GoogleFontsParams,
|
|
||||||
GoogleFontsResponse,
|
|
||||||
} from './google/googleFonts';
|
|
||||||
|
|
||||||
// Fontshare API (DEPRECATED - kept for backward compatibility)
|
|
||||||
export {
|
|
||||||
fetchAllFontshareFonts,
|
|
||||||
fetchFontshareFontBySlug,
|
|
||||||
fetchFontshareFonts,
|
|
||||||
} from './fontshare/fontshare';
|
|
||||||
export type {
|
|
||||||
FontshareParams,
|
|
||||||
FontshareResponse,
|
|
||||||
} from './fontshare/fontshare';
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* Proxy API filters
|
|
||||||
*
|
|
||||||
* Fetches filter metadata from GlyphDiff proxy API.
|
|
||||||
* Provides type-safe response handling.
|
|
||||||
*
|
|
||||||
* @see https://api.glyphdiff.com/api/v1/filters
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { api } from '$shared/api/api';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter metadata type from backend
|
|
||||||
*/
|
|
||||||
export interface FilterMetadata {
|
|
||||||
/** Filter ID (e.g., "providers", "categories", "subsets") */
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
/** Display name (e.g., "Font Providers", "Categories", "Character Subsets") */
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/** Filter description */
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
/** Filter type */
|
|
||||||
type: 'enum' | 'string' | 'array';
|
|
||||||
|
|
||||||
/** Available filter options */
|
|
||||||
options: FilterOption[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter option type
|
|
||||||
*/
|
|
||||||
export interface FilterOption {
|
|
||||||
/** Option ID (e.g., "google", "serif", "latin") */
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
/** Display name (e.g., "Google Fonts", "Serif", "Latin") */
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/** Option value (e.g., "google", "serif", "latin") */
|
|
||||||
value: string;
|
|
||||||
|
|
||||||
/** Number of fonts with this value */
|
|
||||||
count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy filters API response
|
|
||||||
*/
|
|
||||||
export interface ProxyFiltersResponse {
|
|
||||||
/** Array of filter metadata */
|
|
||||||
filters: FilterMetadata[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch filters from proxy API
|
|
||||||
*
|
|
||||||
* @returns Promise resolving to array of filter metadata
|
|
||||||
* @throws ApiError when request fails
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* // Fetch all filters
|
|
||||||
* const filters = await fetchProxyFilters();
|
|
||||||
*
|
|
||||||
* console.log(filters); // [
|
|
||||||
* // { id: "providers", name: "Font Providers", options: [...] },
|
|
||||||
* // { id: "categories", name: "Categories", options: [...] },
|
|
||||||
* // { id: "subsets", name: "Character Subsets", options: [...] }
|
|
||||||
* // ]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export async function fetchProxyFilters(): Promise<FilterMetadata[]> {
|
|
||||||
const response = await api.get<FilterMetadata[]>('/api/v1/filters');
|
|
||||||
|
|
||||||
if (!response.data || !Array.isArray(response.data)) {
|
|
||||||
throw new Error('Proxy API returned invalid response');
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
171
src/entities/Font/api/proxy/proxyFonts.test.ts
Normal file
171
src/entities/Font/api/proxy/proxyFonts.test.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/**
|
||||||
|
* Tests for proxy API client
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
beforeEach,
|
||||||
|
describe,
|
||||||
|
expect,
|
||||||
|
test,
|
||||||
|
vi,
|
||||||
|
} from 'vitest';
|
||||||
|
import type { UnifiedFont } from '../../model/types';
|
||||||
|
import type { ProxyFontsResponse } from './proxyFonts';
|
||||||
|
|
||||||
|
vi.mock('$shared/api/api', () => ({
|
||||||
|
api: {
|
||||||
|
get: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { api } from '$shared/api/api';
|
||||||
|
import {
|
||||||
|
fetchFontsByIds,
|
||||||
|
fetchProxyFontById,
|
||||||
|
fetchProxyFonts,
|
||||||
|
} from './proxyFonts';
|
||||||
|
|
||||||
|
const PROXY_API_URL = 'https://api.glyphdiff.com/api/v1/fonts';
|
||||||
|
|
||||||
|
function createMockFont(overrides: Partial<UnifiedFont> = {}): UnifiedFont {
|
||||||
|
return {
|
||||||
|
id: 'roboto',
|
||||||
|
family: 'Roboto',
|
||||||
|
provider: 'google',
|
||||||
|
category: 'sans-serif',
|
||||||
|
variants: [],
|
||||||
|
subsets: [],
|
||||||
|
...overrides,
|
||||||
|
} as UnifiedFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockApiGet<T>(data: T) {
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce({ data, status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('proxyFonts', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mocked(api.get).mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchProxyFonts', () => {
|
||||||
|
test('should fetch fonts with no params', async () => {
|
||||||
|
const mockResponse: ProxyFontsResponse = {
|
||||||
|
fonts: [createMockFont()],
|
||||||
|
total: 1,
|
||||||
|
limit: 50,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
mockApiGet(mockResponse);
|
||||||
|
|
||||||
|
const result = await fetchProxyFonts();
|
||||||
|
|
||||||
|
expect(api.get).toHaveBeenCalledWith(PROXY_API_URL);
|
||||||
|
expect(result).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should build URL with query params', async () => {
|
||||||
|
const mockResponse: ProxyFontsResponse = {
|
||||||
|
fonts: [createMockFont()],
|
||||||
|
total: 1,
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
mockApiGet(mockResponse);
|
||||||
|
|
||||||
|
await fetchProxyFonts({ provider: 'google', category: 'sans-serif', limit: 20, offset: 0 });
|
||||||
|
|
||||||
|
const calledUrl = vi.mocked(api.get).mock.calls[0][0];
|
||||||
|
expect(calledUrl).toContain('provider=google');
|
||||||
|
expect(calledUrl).toContain('category=sans-serif');
|
||||||
|
expect(calledUrl).toContain('limit=20');
|
||||||
|
expect(calledUrl).toContain('offset=0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw on invalid response (missing fonts array)', async () => {
|
||||||
|
mockApiGet({ total: 0 });
|
||||||
|
|
||||||
|
await expect(fetchProxyFonts()).rejects.toThrow('Proxy API returned invalid response');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw on null response data', async () => {
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce({ data: null, status: 200 });
|
||||||
|
|
||||||
|
await expect(fetchProxyFonts()).rejects.toThrow('Proxy API returned invalid response');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchProxyFontById', () => {
|
||||||
|
test('should return font matching the ID', async () => {
|
||||||
|
const targetFont = createMockFont({ id: 'satoshi', name: 'Satoshi' });
|
||||||
|
const mockResponse: ProxyFontsResponse = {
|
||||||
|
fonts: [createMockFont(), targetFont],
|
||||||
|
total: 2,
|
||||||
|
limit: 1000,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
mockApiGet(mockResponse);
|
||||||
|
|
||||||
|
const result = await fetchProxyFontById('satoshi');
|
||||||
|
|
||||||
|
expect(result).toEqual(targetFont);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined when font not found', async () => {
|
||||||
|
const mockResponse: ProxyFontsResponse = {
|
||||||
|
fonts: [createMockFont()],
|
||||||
|
total: 1,
|
||||||
|
limit: 1000,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
mockApiGet(mockResponse);
|
||||||
|
|
||||||
|
const result = await fetchProxyFontById('nonexistent');
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should search with the ID as query param', async () => {
|
||||||
|
const mockResponse: ProxyFontsResponse = {
|
||||||
|
fonts: [],
|
||||||
|
total: 0,
|
||||||
|
limit: 1000,
|
||||||
|
offset: 0,
|
||||||
|
};
|
||||||
|
mockApiGet(mockResponse);
|
||||||
|
|
||||||
|
await fetchProxyFontById('Roboto');
|
||||||
|
|
||||||
|
const calledUrl = vi.mocked(api.get).mock.calls[0][0];
|
||||||
|
expect(calledUrl).toContain('limit=1000');
|
||||||
|
expect(calledUrl).toContain('q=Roboto');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchFontsByIds', () => {
|
||||||
|
test('should return empty array for empty input', async () => {
|
||||||
|
const result = await fetchFontsByIds([]);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
expect(api.get).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call batch endpoint with comma-separated IDs', async () => {
|
||||||
|
const fonts = [createMockFont({ id: 'roboto' }), createMockFont({ id: 'satoshi' })];
|
||||||
|
mockApiGet(fonts);
|
||||||
|
|
||||||
|
const result = await fetchFontsByIds(['roboto', 'satoshi']);
|
||||||
|
|
||||||
|
expect(api.get).toHaveBeenCalledWith(`${PROXY_API_URL}/batch?ids=roboto,satoshi`);
|
||||||
|
expect(result).toEqual(fonts);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return empty array when response data is nullish', async () => {
|
||||||
|
vi.mocked(api.get).mockResolvedValueOnce({ data: null, status: 200 });
|
||||||
|
|
||||||
|
const result = await fetchFontsByIds(['roboto']);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Proxy API (PRIMARY)
|
// Proxy API (primary)
|
||||||
export {
|
export {
|
||||||
fetchFontsByIds,
|
fetchFontsByIds,
|
||||||
fetchProxyFontById,
|
fetchProxyFontById,
|
||||||
@@ -9,32 +9,9 @@ export type {
|
|||||||
ProxyFontsResponse,
|
ProxyFontsResponse,
|
||||||
} from './api/proxy/proxyFonts';
|
} from './api/proxy/proxyFonts';
|
||||||
|
|
||||||
// Fontshare API (DEPRECATED)
|
|
||||||
export {
|
|
||||||
fetchAllFontshareFonts,
|
|
||||||
fetchFontshareFontBySlug,
|
|
||||||
fetchFontshareFonts,
|
|
||||||
} from './api/fontshare/fontshare';
|
|
||||||
export type {
|
|
||||||
FontshareParams,
|
|
||||||
FontshareResponse,
|
|
||||||
} from './api/fontshare/fontshare';
|
|
||||||
|
|
||||||
// Google Fonts API (DEPRECATED)
|
|
||||||
export {
|
|
||||||
fetchGoogleFontFamily,
|
|
||||||
fetchGoogleFonts,
|
|
||||||
} from './api/google/googleFonts';
|
|
||||||
export type {
|
|
||||||
GoogleFontItem,
|
|
||||||
GoogleFontsParams,
|
|
||||||
GoogleFontsResponse,
|
|
||||||
} from './api/google/googleFonts';
|
|
||||||
export {
|
export {
|
||||||
normalizeFontshareFont,
|
normalizeFontshareFont,
|
||||||
normalizeFontshareFonts,
|
normalizeFontshareFonts,
|
||||||
normalizeGoogleFont,
|
|
||||||
normalizeGoogleFonts,
|
|
||||||
} from './lib/normalize/normalize';
|
} from './lib/normalize/normalize';
|
||||||
export type {
|
export type {
|
||||||
// Domain types
|
// Domain types
|
||||||
@@ -65,8 +42,6 @@ export type {
|
|||||||
FontVariant,
|
FontVariant,
|
||||||
FontWeight,
|
FontWeight,
|
||||||
FontWeightItalic,
|
FontWeightItalic,
|
||||||
// Google Fonts API types
|
|
||||||
GoogleFontsApiModel,
|
|
||||||
// Normalization types
|
// Normalization types
|
||||||
UnifiedFont,
|
UnifiedFont,
|
||||||
UnifiedFontVariant,
|
UnifiedFontVariant,
|
||||||
|
|||||||
@@ -3,13 +3,31 @@ import type {
|
|||||||
UnifiedFont,
|
UnifiedFont,
|
||||||
} from '../../model';
|
} from '../../model';
|
||||||
|
|
||||||
|
/** Valid font weight values (100-900 in increments of 100) */
|
||||||
const SIZES = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
const SIZES = [100, 200, 300, 400, 500, 600, 700, 800, 900];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a URL for a font based on the provided font and weight.
|
* Gets the URL for a font file at a specific weight
|
||||||
* @param font - The font object.
|
*
|
||||||
* @param weight - The weight of the font.
|
* Constructs the appropriate URL for loading a font file based on
|
||||||
* @returns The URL for the font.
|
* the font object and requested weight. Handles variable fonts and
|
||||||
|
* provides fallbacks for static fonts.
|
||||||
|
*
|
||||||
|
* @param font - Unified font object containing style URLs
|
||||||
|
* @param weight - Font weight (100-900)
|
||||||
|
* @returns URL string for the font file, or undefined if not found
|
||||||
|
* @throws Error if weight is not a valid value (100-900)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const url = getFontUrl(roboto, 700); // Returns URL for Roboto Bold
|
||||||
|
*
|
||||||
|
* // Variable fonts: backend maps weight to VF URL
|
||||||
|
* const vfUrl = getFontUrl(inter, 450); // Returns variable font URL
|
||||||
|
*
|
||||||
|
* // Fallback for missing weights
|
||||||
|
* const fallback = getFontUrl(font, 900); // Falls back to regular/400 if 900 missing
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function getFontUrl(font: UnifiedFont, weight: number): string | undefined {
|
export function getFontUrl(font: UnifiedFont, weight: number): string | undefined {
|
||||||
if (!SIZES.includes(weight)) {
|
if (!SIZES.includes(weight)) {
|
||||||
@@ -18,12 +36,11 @@ export function getFontUrl(font: UnifiedFont, weight: number): string | undefine
|
|||||||
|
|
||||||
const weightKey = weight.toString() as FontWeight;
|
const weightKey = weight.toString() as FontWeight;
|
||||||
|
|
||||||
// 1. Try exact match (Backend now maps "100".."900" to VF URL if variable)
|
// Try exact match (backend maps weight to VF URL for variable fonts)
|
||||||
if (font.styles.variants?.[weightKey]) {
|
if (font.styles.variants?.[weightKey]) {
|
||||||
return font.styles.variants[weightKey];
|
return font.styles.variants[weightKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Fallbacks for Static Fonts (if exact weight missing)
|
// Fallbacks for static fonts when exact weight is missing
|
||||||
// Try 'regular' or '400' as safe defaults
|
|
||||||
return font.styles.regular || font.styles.variants?.['400'] || font.styles.variants?.['regular'];
|
return font.styles.regular || font.styles.variants?.['400'] || font.styles.variants?.['regular'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* ============================================================================
|
* Mock font filter data
|
||||||
* MOCK FONT FILTER DATA
|
|
||||||
* ============================================================================
|
|
||||||
*
|
*
|
||||||
* Factory functions and preset mock data for font-related filters.
|
* Factory functions and preset mock data for font-related filters.
|
||||||
* Used in Storybook stories for font filtering components.
|
* Used in Storybook stories for font filtering components.
|
||||||
@@ -36,9 +34,7 @@ import type {
|
|||||||
import type { Property } from '$shared/lib';
|
import type { Property } from '$shared/lib';
|
||||||
import { createFilter } from '$shared/lib';
|
import { createFilter } from '$shared/lib';
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TYPE DEFINITIONS
|
// TYPE DEFINITIONS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for creating a mock filter
|
* Options for creating a mock filter
|
||||||
@@ -60,9 +56,7 @@ export interface MockFilters {
|
|||||||
subsets: ReturnType<typeof createFilter<FontSubset>>;
|
subsets: ReturnType<typeof createFilter<FontSubset>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// FONT CATEGORIES
|
// FONT CATEGORIES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Fonts categories
|
* Google Fonts categories
|
||||||
@@ -98,9 +92,7 @@ export const UNIFIED_CATEGORIES: Property<FontCategory>[] = [
|
|||||||
{ id: 'monospace', name: 'Monospace', value: 'monospace' },
|
{ id: 'monospace', name: 'Monospace', value: 'monospace' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// FONT SUBSETS
|
// FONT SUBSETS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Common font subsets
|
* Common font subsets
|
||||||
@@ -114,9 +106,7 @@ export const FONT_SUBSETS: Property<FontSubset>[] = [
|
|||||||
{ id: 'devanagari', name: 'Devanagari', value: 'devanagari' },
|
{ id: 'devanagari', name: 'Devanagari', value: 'devanagari' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// FONT PROVIDERS
|
// FONT PROVIDERS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font providers
|
* Font providers
|
||||||
@@ -126,9 +116,7 @@ export const FONT_PROVIDERS: Property<FontProvider>[] = [
|
|||||||
{ id: 'fontshare', name: 'Fontshare', value: 'fontshare' },
|
{ id: 'fontshare', name: 'Fontshare', value: 'fontshare' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// FILTER FACTORIES
|
// FILTER FACTORIES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mock filter from properties
|
* Create a mock filter from properties
|
||||||
@@ -172,9 +160,7 @@ export function createProvidersFilter(options?: { selected?: FontProvider[] }) {
|
|||||||
return createFilter<FontProvider>({ properties });
|
return createFilter<FontProvider>({ properties });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PRESET FILTERS
|
// PRESET FILTERS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preset mock filters - use these directly in stories
|
* Preset mock filters - use these directly in stories
|
||||||
@@ -251,9 +237,7 @@ export const MOCK_FILTERS_ALL_SELECTED: MockFilters = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// GENERIC FILTER MOCKS
|
// GENERIC FILTER MOCKS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mock filter with generic string properties
|
* Create a mock filter with generic string properties
|
||||||
|
|||||||
@@ -50,9 +50,7 @@ import type {
|
|||||||
UnifiedFont,
|
UnifiedFont,
|
||||||
} from '$entities/Font/model/types';
|
} from '$entities/Font/model/types';
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// GOOGLE FONTS MOCKS
|
// GOOGLE FONTS MOCKS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for creating a mock Google Font
|
* Options for creating a mock Google Font
|
||||||
@@ -186,9 +184,7 @@ export const GOOGLE_FONTS: Record<string, GoogleFontItem> = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// FONTHARE MOCKS
|
// FONTHARE MOCKS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for creating a mock Fontshare font
|
* Options for creating a mock Fontshare font
|
||||||
@@ -399,9 +395,7 @@ export const FONTHARE_FONTS: Record<string, FontshareFont> = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// UNIFIED FONT MOCKS
|
// UNIFIED FONT MOCKS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for creating a mock UnifiedFont
|
* Options for creating a mock UnifiedFont
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ import {
|
|||||||
generateMockFonts,
|
generateMockFonts,
|
||||||
} from './fonts.mock';
|
} from './fonts.mock';
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TANSTACK QUERY MOCK TYPES
|
// TANSTACK QUERY MOCK TYPES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock TanStack Query state
|
* Mock TanStack Query state
|
||||||
@@ -83,9 +81,7 @@ export interface MockQueryObserverResult<TData = unknown, TError = Error> {
|
|||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// TANSTACK QUERY MOCK FACTORIES
|
// TANSTACK QUERY MOCK FACTORIES
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mock query state for TanStack Query
|
* Create a mock query state for TanStack Query
|
||||||
@@ -142,9 +138,7 @@ export function createSuccessState<TData>(data: TData): MockQueryObserverResult<
|
|||||||
return createMockQueryState<TData>({ status: 'success', data, error: undefined });
|
return createMockQueryState<TData>({ status: 'success', data, error: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// FONT STORE MOCKS
|
// FONT STORE MOCKS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock UnifiedFontStore state
|
* Mock UnifiedFontStore state
|
||||||
@@ -332,9 +326,7 @@ export const MOCK_FONT_STORE_STATES = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MOCK STORE OBJECT
|
// MOCK STORE OBJECT
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a mock store object that mimics TanStack Query behavior
|
* Create a mock store object that mimics TanStack Query behavior
|
||||||
@@ -469,9 +461,7 @@ export const MOCK_STORES = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// REACTIVE STATE MOCKS
|
// REACTIVE STATE MOCKS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a reactive state object using Svelte 5 runes pattern
|
* Create a reactive state object using Svelte 5 runes pattern
|
||||||
@@ -525,9 +515,7 @@ export function createMockComparisonStore(config: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// MOCK DATA GENERATORS
|
// MOCK DATA GENERATORS
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate paginated font data
|
* Generate paginated font data
|
||||||
|
|||||||
@@ -7,41 +7,64 @@ import {
|
|||||||
} from '@tanstack/query-core';
|
} from '@tanstack/query-core';
|
||||||
import type { UnifiedFont } from '../types';
|
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>> {
|
export abstract class BaseFontStore<TParams extends Record<string, any>> {
|
||||||
|
/**
|
||||||
|
* Cleanup function for effects
|
||||||
|
* Call destroy() to remove effects and prevent memory leaks
|
||||||
|
*/
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
|
|
||||||
|
/** Reactive parameter bindings from external sources */
|
||||||
#bindings = $state<(() => Partial<TParams>)[]>([]);
|
#bindings = $state<(() => Partial<TParams>)[]>([]);
|
||||||
|
/** Internal parameter state */
|
||||||
#internalParams = $state<TParams>({} as TParams);
|
#internalParams = $state<TParams>({} as TParams);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merged params from internal state and all bindings
|
||||||
|
* Automatically updates when bindings or internal params change
|
||||||
|
*/
|
||||||
params = $derived.by(() => {
|
params = $derived.by(() => {
|
||||||
let merged = { ...this.#internalParams };
|
let merged = { ...this.#internalParams };
|
||||||
|
|
||||||
// Loop through every "Cable" plugged into the store
|
// Merge all binding results into params
|
||||||
// Loop through every "Cable" plugged into the store
|
|
||||||
for (const getter of this.#bindings) {
|
for (const getter of this.#bindings) {
|
||||||
const bindingResult = getter();
|
const bindingResult = getter();
|
||||||
merged = { ...merged, ...bindingResult };
|
merged = { ...merged, ...bindingResult };
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged as TParams;
|
return merged as TParams;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** TanStack Query result state */
|
||||||
protected result = $state<QueryObserverResult<UnifiedFont[], Error>>({} as any);
|
protected result = $state<QueryObserverResult<UnifiedFont[], Error>>({} as any);
|
||||||
|
/** TanStack Query observer instance */
|
||||||
protected observer: QueryObserver<UnifiedFont[], Error>;
|
protected observer: QueryObserver<UnifiedFont[], Error>;
|
||||||
|
/** Shared query client */
|
||||||
protected qc = queryClient;
|
protected qc = queryClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new base font store
|
||||||
|
* @param initialParams - Initial query parameters
|
||||||
|
*/
|
||||||
constructor(initialParams: TParams) {
|
constructor(initialParams: TParams) {
|
||||||
this.#internalParams = initialParams;
|
this.#internalParams = initialParams;
|
||||||
|
|
||||||
this.observer = new QueryObserver(this.qc, this.getOptions());
|
this.observer = new QueryObserver(this.qc, this.getOptions());
|
||||||
|
|
||||||
// Sync TanStack -> Svelte State
|
// Sync TanStack Query state -> Svelte state
|
||||||
this.observer.subscribe(r => {
|
this.observer.subscribe(r => {
|
||||||
this.result = r;
|
this.result = r;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync Svelte State -> TanStack Options
|
// Sync Svelte state changes -> TanStack Query options
|
||||||
this.cleanup = $effect.root(() => {
|
this.cleanup = $effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
this.observer.setOptions(this.getOptions());
|
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;
|
protected abstract getQueryKey(params: TParams): QueryKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Must be implemented by child class
|
||||||
|
* Fetches font data from API
|
||||||
|
*/
|
||||||
protected abstract fetchFn(params: TParams): Promise<UnifiedFont[]>;
|
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> {
|
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
|
||||||
return {
|
return {
|
||||||
queryKey: this.getQueryKey(params),
|
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() {
|
get fonts() {
|
||||||
return this.result.data ?? [];
|
return this.result.data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether currently fetching initial data */
|
||||||
get isLoading() {
|
get isLoading() {
|
||||||
return this.result.isLoading;
|
return this.result.isLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether any fetch is in progress (including refetches) */
|
||||||
get isFetching() {
|
get isFetching() {
|
||||||
return this.result.isFetching;
|
return this.result.isFetching;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether last fetch resulted in an error */
|
||||||
get isError() {
|
get isError() {
|
||||||
return this.result.isError;
|
return this.result.isError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether no fonts are loaded (not loading and empty array) */
|
||||||
get isEmpty() {
|
get isEmpty() {
|
||||||
return !this.isLoading && this.fonts.length === 0;
|
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>) {
|
addBinding(getter: () => Partial<TParams>) {
|
||||||
this.#bindings.push(getter);
|
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>) {
|
setParams(newParams: Partial<TParams>) {
|
||||||
this.#internalParams = { ...this.params, ...newParams };
|
this.#internalParams = { ...this.params, ...newParams };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalidate cache and refetch
|
* 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) });
|
this.qc.invalidateQueries({ queryKey: this.getQueryKey(this.params) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up effects and observers
|
||||||
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually refetch
|
* Manually trigger a refetch
|
||||||
*/
|
*/
|
||||||
async refetch() {
|
async refetch() {
|
||||||
await this.observer.refetch();
|
await this.observer.refetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefetch with different params (for hover states, pagination, etc.)
|
* Prefetch data with different parameters
|
||||||
*/
|
*/
|
||||||
async prefetch(params: TParams) {
|
async prefetch(params: TParams) {
|
||||||
await this.qc.prefetchQuery(this.getOptions(params));
|
await this.qc.prefetchQuery(this.getOptions(params));
|
||||||
|
|||||||
@@ -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
|
* // Update parameters
|
||||||
* store.setCategory('serif');
|
* store.setCategories(['serif']);
|
||||||
* store.nextPage();
|
* store.nextPage();
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@@ -108,16 +108,20 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
this.#filterCleanup = $effect.root(() => {
|
this.#filterCleanup = $effect.root(() => {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const filterParams = JSON.stringify({
|
const filterParams = JSON.stringify({
|
||||||
provider: this.params.provider,
|
providers: this.params.providers,
|
||||||
category: this.params.category,
|
categories: this.params.categories,
|
||||||
subset: this.params.subset,
|
subsets: this.params.subsets,
|
||||||
q: this.params.q,
|
q: this.params.q,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If filters changed, reset offset to 0
|
// If filters changed, reset offset and invalidate cache
|
||||||
if (filterParams !== this.#previousFilterParams) {
|
if (filterParams !== this.#previousFilterParams) {
|
||||||
if (this.#previousFilterParams && this.params.offset !== 0) {
|
if (this.#previousFilterParams) {
|
||||||
this.setParams({ offset: 0 });
|
if (this.params.offset !== 0) {
|
||||||
|
this.setParams({ offset: 0 });
|
||||||
|
}
|
||||||
|
this.#accumulatedFonts = [];
|
||||||
|
this.invalidate();
|
||||||
}
|
}
|
||||||
this.#previousFilterParams = filterParams;
|
this.#previousFilterParams = filterParams;
|
||||||
}
|
}
|
||||||
@@ -170,7 +174,7 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
|
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 {
|
return {
|
||||||
queryKey: this.getQueryKey(params),
|
queryKey: this.getQueryKey(params),
|
||||||
queryFn: () => this.fetchFn(params),
|
queryFn: () => this.fetchFn(params),
|
||||||
@@ -221,8 +225,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
return response.fonts;
|
return response.fonts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Getters (proxied from BaseFontStore) ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all accumulated fonts (for infinite scroll)
|
* Get all accumulated fonts (for infinite scroll)
|
||||||
*/
|
*/
|
||||||
@@ -258,27 +260,25 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
return !this.isLoading && this.fonts.length === 0;
|
return !this.isLoading && this.fonts.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Provider-specific shortcuts ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set provider filter
|
* Set providers filter
|
||||||
*/
|
*/
|
||||||
setProvider(provider: 'google' | 'fontshare' | undefined) {
|
setProviders(providers: ProxyFontsParams['providers']) {
|
||||||
this.setParams({ provider });
|
this.setParams({ providers });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set category filter
|
* Set categories filter
|
||||||
*/
|
*/
|
||||||
setCategory(category: ProxyFontsParams['category']) {
|
setCategories(categories: ProxyFontsParams['categories']) {
|
||||||
this.setParams({ category });
|
this.setParams({ categories });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set subset filter
|
* Set subsets filter
|
||||||
*/
|
*/
|
||||||
setSubset(subset: ProxyFontsParams['subset']) {
|
setSubsets(subsets: ProxyFontsParams['subsets']) {
|
||||||
this.setParams({ subset });
|
this.setParams({ subsets });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,8 +295,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
this.setParams({ sort });
|
this.setParams({ sort });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Pagination methods ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Go to next page
|
* Go to next page
|
||||||
*/
|
*/
|
||||||
@@ -337,8 +335,6 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
this.setParams({ limit });
|
this.setParams({ limit });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Category shortcuts (for convenience) ---
|
|
||||||
|
|
||||||
get sansSerifFonts() {
|
get sansSerifFonts() {
|
||||||
return this.fonts.filter(f => f.category === 'sans-serif');
|
return this.fonts.filter(f => f.category === 'sans-serif');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,60 @@
|
|||||||
/**
|
/**
|
||||||
* ============================================================================
|
* Common font domain types
|
||||||
* 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 FontshareFontCategory } from './fontshare';
|
||||||
import type { FontCategory as GoogleFontCategory } from './google';
|
import type { FontCategory as GoogleFontCategory } from './google';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font category
|
* Unified font category across all providers
|
||||||
*/
|
*/
|
||||||
export type FontCategory = GoogleFontCategory | FontshareFontCategory;
|
export type FontCategory = GoogleFontCategory | FontshareFontCategory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font provider
|
* Font provider identifier
|
||||||
*/
|
*/
|
||||||
export type FontProvider = 'google' | 'fontshare';
|
export type FontProvider = 'google' | 'fontshare';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Font subset
|
* Character subset support
|
||||||
*/
|
*/
|
||||||
export type FontSubset = 'latin' | 'latin-ext' | 'cyrillic' | 'greek' | 'arabic' | 'devanagari';
|
export type FontSubset = 'latin' | 'latin-ext' | 'cyrillic' | 'greek' | 'arabic' | 'devanagari';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter state
|
* Combined filter state for font queries
|
||||||
*/
|
*/
|
||||||
export interface FontFilters {
|
export interface FontFilters {
|
||||||
|
/** Selected font providers */
|
||||||
providers: FontProvider[];
|
providers: FontProvider[];
|
||||||
|
/** Selected font categories */
|
||||||
categories: FontCategory[];
|
categories: FontCategory[];
|
||||||
|
/** Selected character subsets */
|
||||||
subsets: FontSubset[];
|
subsets: FontSubset[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Filter group identifier */
|
||||||
export type FilterGroup = 'providers' | 'categories' | 'subsets';
|
export type FilterGroup = 'providers' | 'categories' | 'subsets';
|
||||||
|
|
||||||
|
/** Filter type including search query */
|
||||||
export type FilterType = FilterGroup | 'searchQuery';
|
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';
|
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`;
|
export type FontWeightItalic = `${FontWeight}italic`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All possible font variants
|
* All possible font variant identifiers
|
||||||
|
*
|
||||||
|
* Includes:
|
||||||
* - Numeric weights: "400", "700", etc.
|
* - Numeric weights: "400", "700", etc.
|
||||||
* - Italic variants: "400italic", "700italic", etc.
|
* - Italic variants: "400italic", "700italic", etc.
|
||||||
* - Legacy names: "regular", "italic", "bold", "bolditalic"
|
* - Legacy names: "regular", "italic", "bold", "bolditalic"
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ export interface FontMetadata {
|
|||||||
lastModified?: string;
|
lastModified?: string;
|
||||||
/** Popularity rank (if available from provider) */
|
/** Popularity rank (if available from provider) */
|
||||||
popularity?: number;
|
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;
|
name: string;
|
||||||
/** Font provider (google | fontshare) */
|
/** Font provider (google | fontshare) */
|
||||||
provider: FontProvider;
|
provider: FontProvider;
|
||||||
|
/**
|
||||||
|
* Provider badge display name
|
||||||
|
*
|
||||||
|
* Human-readable provider name for UI display
|
||||||
|
* e.g., "Google Fonts" or "Fontshare"
|
||||||
|
*/
|
||||||
|
providerBadge?: string;
|
||||||
/** Font category classification */
|
/** Font category classification */
|
||||||
category: FontCategory;
|
category: FontCategory;
|
||||||
/** Supported character subsets */
|
/** Supported character subsets */
|
||||||
|
|||||||
@@ -16,19 +16,20 @@ import {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
* Applied font
|
* Font to apply
|
||||||
*/
|
*/
|
||||||
font: UnifiedFont;
|
font: UnifiedFont;
|
||||||
/**
|
/**
|
||||||
* Font weight
|
* Font weight
|
||||||
|
* @default 400
|
||||||
*/
|
*/
|
||||||
weight?: number;
|
weight?: number;
|
||||||
/**
|
/**
|
||||||
* Additional classes
|
* CSS classes
|
||||||
*/
|
*/
|
||||||
className?: string;
|
className?: string;
|
||||||
/**
|
/**
|
||||||
* Children
|
* Content snippet
|
||||||
*/
|
*/
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
@@ -44,7 +45,7 @@ const status = $derived(
|
|||||||
appliedFontsManager.getFontStatus(
|
appliedFontsManager.getFontStatus(
|
||||||
font.id,
|
font.id,
|
||||||
weight,
|
weight,
|
||||||
font.features.isVariable,
|
font.features?.isVariable,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ interface Props extends
|
|||||||
>
|
>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Callback for when visible items change
|
* Visible items callback
|
||||||
*/
|
*/
|
||||||
onVisibleItemsChange?: (items: UnifiedFont[]) => void;
|
onVisibleItemsChange?: (items: UnifiedFont[]) => void;
|
||||||
/**
|
/**
|
||||||
* Weight of the font
|
* Font weight
|
||||||
*/
|
*/
|
||||||
weight: number;
|
weight: number;
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +69,7 @@ function handleInternalVisibleChange(visibleItems: UnifiedFont[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-register fonts with the manager
|
// Auto-register fonts with the manager
|
||||||
appliedFontsManager.touch(configs);
|
appliedFontsManager.touch(configs);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user