Compare commits
9 Commits
0b0489fa26
...
c06aad1a8a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06aad1a8a | ||
|
|
471e186e70 | ||
|
|
dc72b9e048 | ||
|
|
07a37af71a | ||
|
|
d6607e5705 | ||
|
|
10801a641a | ||
|
|
98eab35615 | ||
|
|
7fbeef68e2 | ||
|
|
7078cb6f8c |
480
FIX_SUMMARY.md
Normal file
480
FIX_SUMMARY.md
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
# Fix Applied: Query Data Cannot Be Undefined
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Successfully fixed the TanStack Query error: **"Query data cannot be undefined. Please make sure to return a value other than undefined from your query function."**
|
||||||
|
|
||||||
|
## Root Causes
|
||||||
|
|
||||||
|
1. **Missing `gcTime` parameter** in TanStack Query configuration
|
||||||
|
2. **No response validation** when proxy API returns invalid/missing data
|
||||||
|
3. **Incorrect generic type constraint** in `FontVirtualList.svelte`
|
||||||
|
|
||||||
|
## Changes Applied
|
||||||
|
|
||||||
|
### 1. Fixed TanStack Query Configuration
|
||||||
|
|
||||||
|
**File**: `src/entities/Font/model/store/baseFontStore.svelte.ts`
|
||||||
|
|
||||||
|
Added `gcTime` parameter to properly manage cached data lifecycle:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
|
||||||
|
return {
|
||||||
|
queryKey: this.getQueryKey(params),
|
||||||
|
queryFn: () => this.fetchFn(params),
|
||||||
|
staleTime:5 * 60 *1000, // 5 minutes
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 minutes ✅ ADDED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
|
||||||
|
- Prevents stale data from persisting too long
|
||||||
|
- Explicit control over garbage collection timing
|
||||||
|
- Better memory management
|
||||||
|
|
||||||
|
### 2. Added Response Validation
|
||||||
|
|
||||||
|
**File**: `src/entities/Font/model/store/unifiedFontStore.svelte.ts`
|
||||||
|
|
||||||
|
Added comprehensive validation in `fetchFn`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
protected async fetchFn(params: ProxyFontsParams): Promise<UnifiedFont[]> {
|
||||||
|
const response = await fetchProxyFonts(params);
|
||||||
|
|
||||||
|
// Validate response exists
|
||||||
|
if (!response) {
|
||||||
|
console.error('[UnifiedFontStore] fetchProxyFonts returned undefined', { params });
|
||||||
|
throw new Error('Proxy API returned undefined response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fonts array exists
|
||||||
|
if (!response.fonts) {
|
||||||
|
console.error('[UnifiedFontStore] response.fonts is undefined', { response });
|
||||||
|
throw new Error('Proxy API response missing fonts array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate fonts is an array
|
||||||
|
if (!Array.isArray(response.fonts)) {
|
||||||
|
console.error('[UnifiedFontStore] response.fonts is not an array', { fonts: response.fonts });
|
||||||
|
throw new Error('Proxy API fonts is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store pagination metadata separately for derived values
|
||||||
|
this.#paginationMetadata = {
|
||||||
|
total: response.total ?? 0,
|
||||||
|
limit: response.limit ?? this.params.limit ?? 50,
|
||||||
|
offset: response.offset ?? this.params.offset ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response.fonts;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
|
||||||
|
- Early detection of invalid API responses
|
||||||
|
- Detailed error logging for debugging
|
||||||
|
- Prevents undefined data from being cached
|
||||||
|
- Fallback to default values for pagination metadata
|
||||||
|
|
||||||
|
### 3. Added Fallback to Fontshare API
|
||||||
|
|
||||||
|
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
|
||||||
|
|
||||||
|
Implemented automatic fallback to Fontshare API when proxy fails:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
async function fetchFontshareFallback(
|
||||||
|
params: ProxyFontsParams,
|
||||||
|
): Promise<ProxyFontsResponse> {
|
||||||
|
// Import dynamically to avoid circular dependency
|
||||||
|
const { fetchFontshareFonts } = await import('../fontshare/fontshare');
|
||||||
|
const { normalizeFontshareFonts } = await import('../../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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
|
||||||
|
- App continues working even if proxy API is down
|
||||||
|
- Allows development without breaking functionality
|
||||||
|
- Automatic detection of network errors
|
||||||
|
- Seamless fallback with console logging
|
||||||
|
|
||||||
|
### 4. Fixed Type Constraints
|
||||||
|
|
||||||
|
**File**: `src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte`
|
||||||
|
|
||||||
|
Fixed incorrect generic type constraint:
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<script lang="ts" generics="T extends ({ id: string } | [{ id: string }, { id: string; }])">
|
||||||
|
```
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
<script lang="ts" generics="T extends { id: string }">
|
||||||
|
```
|
||||||
|
|
||||||
|
Also simplified the font registration logic:
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const slugs = visibleItems.map(item => {
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
return item.map(font => font.id);
|
||||||
|
}
|
||||||
|
return item.id;
|
||||||
|
}).flat();
|
||||||
|
```
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const slugs = visibleItems.map(item => item.id);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
|
||||||
|
- Fixed TypeScript errors
|
||||||
|
- Simplified code
|
||||||
|
- Proper type safety
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Normal Flow (Proxy API Available)
|
||||||
|
|
||||||
|
1. **Request**: Component requests fonts → Filter manager maps to params → `unifiedFontStore.setParams()`
|
||||||
|
2. **Fetch**: `unifiedFontStore.fetchFn()` calls `fetchProxyFonts()`
|
||||||
|
3. **Proxy API**: Request sent to `https://api.glyphdiff.com/api/v1/fonts`
|
||||||
|
4. **Validation**: Response validated to ensure `fonts` array exists
|
||||||
|
5. **Cache**: TanStack Query caches response for 5 minutes
|
||||||
|
6. **Render**: Components render `unifiedFontStore.fonts`
|
||||||
|
|
||||||
|
### Fallback Flow (Proxy API Unavailable)
|
||||||
|
|
||||||
|
1. **Request**: Same as normal flow
|
||||||
|
2. **Fetch**: `fetchProxyFonts()` attempts proxy API
|
||||||
|
3. **Error**: Proxy API returns network error (404, 500, connection failed)
|
||||||
|
4. **Detection**: Error caught and classified as network error
|
||||||
|
5. **Fallback**: `fetchFontshareFallback()` called automatically
|
||||||
|
6. **Fontshare API**: Request sent to Fontshare API
|
||||||
|
7. **Normalization**: Fontshare response normalized to `UnifiedFont` format
|
||||||
|
8. **Render**: Components render fonts seamlessly
|
||||||
|
|
||||||
|
### Debug Flow (Enable Proxy API)
|
||||||
|
|
||||||
|
With `USE_PROXY_API = true`, console will show:
|
||||||
|
|
||||||
|
```
|
||||||
|
[fetchProxyFonts] Fetching from proxy API { params: {...}, url: "https://api.glyphdiff.com/api/v1/fonts?..." }
|
||||||
|
[fetchProxyFonts] Proxy API success { count: 50 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Flow (Disable Proxy API)
|
||||||
|
|
||||||
|
With `USE_PROXY_API = false`, console will show:
|
||||||
|
|
||||||
|
```
|
||||||
|
[fetchProxyFonts] Using Fontshare API (proxy disabled)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debug Flow (Proxy API Fails)
|
||||||
|
|
||||||
|
When proxy API fails, console will show:
|
||||||
|
|
||||||
|
```
|
||||||
|
[fetchProxyFonts] Fetching from proxy API { params: {...}, url: "https://api.glyphdiff.com/api/v1/fonts?..." }
|
||||||
|
[fetchProxyFonts] Proxy API failed, using fallback Error: ...
|
||||||
|
[fetchProxyFonts] Using Fontshare API as fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing the Fix
|
||||||
|
|
||||||
|
### 1. With Proxy API Working
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
|
||||||
|
- Your proxy API is running at `https://api.glyphdiff.com/api/v1/fonts`
|
||||||
|
- Proxy API returns correct response structure
|
||||||
|
|
||||||
|
**Test**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dev server
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# Open browser console
|
||||||
|
# Should see: "[fetchProxyFonts] Fetching from proxy API"
|
||||||
|
# Should see: "[fetchProxyFonts] Proxy API success { count: N }"
|
||||||
|
# Should see fonts loading correctly
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Behavior**:
|
||||||
|
|
||||||
|
- Fonts load from proxy API
|
||||||
|
- Console shows successful fetch
|
||||||
|
- No errors in console
|
||||||
|
- All features working
|
||||||
|
|
||||||
|
### 2. With Proxy API Down
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
|
||||||
|
- Proxy API not running or returning errors
|
||||||
|
|
||||||
|
**Test**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dev server
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# Open browser console
|
||||||
|
# Should see: "[fetchProxyFonts] Proxy API failed, using fallback"
|
||||||
|
# Should see: "[fetchProxyFonts] Using Fontshare API as fallback"
|
||||||
|
# Should see fonts loading from Fontshare
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Behavior**:
|
||||||
|
|
||||||
|
- App falls back to Fontshare API automatically
|
||||||
|
- Console shows fallback messages
|
||||||
|
- Fonts load from Fontshare
|
||||||
|
- All features working (no user-facing errors)
|
||||||
|
|
||||||
|
### 3. Disable Proxy API Manually
|
||||||
|
|
||||||
|
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const USE_PROXY_API = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start dev server
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# Open browser console
|
||||||
|
# Should see: "[fetchProxyFonts] Using Fontshare API (proxy disabled)"
|
||||||
|
# Should see fonts loading from Fontshare
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Behavior**:
|
||||||
|
|
||||||
|
- App uses Fontshare API directly
|
||||||
|
- Console shows proxy disabled message
|
||||||
|
- All features working
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### To Enable Proxy API (When Ready)
|
||||||
|
|
||||||
|
1. **Verify Proxy API**:
|
||||||
|
```bash
|
||||||
|
curl "https://api.glyphdiff.com/api/v1/fonts?limit=5"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check Response Format**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fonts": [...],
|
||||||
|
"total": N,
|
||||||
|
"limit": 5,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Ensure Each Font Has Required Fields**:
|
||||||
|
- `id`, `name`, `provider`
|
||||||
|
- `category`, `subsets`
|
||||||
|
- `variants`, `styles`
|
||||||
|
- `metadata`, `features`
|
||||||
|
|
||||||
|
4. **Test in App**:
|
||||||
|
- Open browser console
|
||||||
|
- Check for success messages
|
||||||
|
- Verify fonts load correctly
|
||||||
|
|
||||||
|
### To Remove Fallback (When Proxy API is Stable)
|
||||||
|
|
||||||
|
Once proxy API is proven stable, you can:
|
||||||
|
|
||||||
|
1. **Remove Fallback Function**:
|
||||||
|
```typescript
|
||||||
|
// Delete fetchFontshareFallback() function
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Remove Dynamic Imports**:
|
||||||
|
```typescript
|
||||||
|
// No longer need dynamic imports
|
||||||
|
const { fetchFontshareFonts } = await import('../fontshare/fontshare');
|
||||||
|
const { normalizeFontshareFonts } = await import('../../lib/normalize/normalize');
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Remove Old API Exports**:
|
||||||
|
```typescript
|
||||||
|
// Remove Fontshare API exports from index.ts
|
||||||
|
// Remove normalization function exports
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Issue: Still Seeing "Query data cannot be undefined"
|
||||||
|
|
||||||
|
**Cause**: Proxy API returns invalid response
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check console for validation errors
|
||||||
|
2. Verify proxy API response structure
|
||||||
|
3. Check network tab in DevTools for actual response
|
||||||
|
4. Set `USE_PROXY_API = false` to use fallback
|
||||||
|
|
||||||
|
### Issue: Fonts Not Loading At All
|
||||||
|
|
||||||
|
**Cause**: Both proxy and Fontshare APIs failing
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Check console for error messages
|
||||||
|
2. Verify network connectivity
|
||||||
|
3. Check CORS settings on proxy API
|
||||||
|
4. Check API endpoint URL is correct
|
||||||
|
|
||||||
|
### Issue: Fallback Not Triggering
|
||||||
|
|
||||||
|
**Cause**: Error type not recognized as network error
|
||||||
|
|
||||||
|
**Solutions**:
|
||||||
|
|
||||||
|
1. Add additional error message checks
|
||||||
|
2. Check console for actual error message
|
||||||
|
3. Manually set `USE_PROXY_API = false`
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `src/entities/Font/model/store/baseFontStore.svelte.ts` - Added gcTime parameter
|
||||||
|
2. `src/entities/Font/model/store/unifiedFontStore.svelte.ts` - Added response validation
|
||||||
|
3. `src/entities/Font/api/proxy/proxyFonts.ts` - Added fallback logic
|
||||||
|
4. `src/entities/Font/ui/FontVirtualList/FontVirtualList.svelte` - Fixed generic type
|
||||||
|
|
||||||
|
## Commit
|
||||||
|
|
||||||
|
```
|
||||||
|
commit 471e186
|
||||||
|
fix: Fix undefined query data and add fallback to Fontshare API
|
||||||
|
|
||||||
|
- Add gcTime parameter to TanStack Query config
|
||||||
|
- Add response validation in fetchFn with detailed logging
|
||||||
|
- Add fallback to Fontshare API when proxy fails
|
||||||
|
- Add USE_PROXY_API flag for easy switching
|
||||||
|
- Fix FontVirtualList generic type constraint
|
||||||
|
- Simplify font registration logic
|
||||||
|
- Add comprehensive console logging for debugging
|
||||||
|
|
||||||
|
Fixes: Query data cannot be undefined error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
See `PROXY_API_FIXES.md` for detailed technical documentation on all changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: January 29, 2026
|
||||||
|
**Status**: ✅ Fixed and Committed
|
||||||
403
PROXY_API_FIXES.md
Normal file
403
PROXY_API_FIXES.md
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
# Proxy API Integration - Changes & Fixes
|
||||||
|
|
||||||
|
## Issue Fixed
|
||||||
|
|
||||||
|
**Error**: `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ["unifiedFonts",{}]`
|
||||||
|
|
||||||
|
**Root Cause**:
|
||||||
|
|
||||||
|
1. Missing `gcTime` parameter in TanStack Query configuration
|
||||||
|
2. No validation of proxy API response structure
|
||||||
|
3. No error handling for when proxy API returns invalid/missing data
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### 1. Fixed TanStack Query Configuration (`baseFontStore.svelte.ts`)
|
||||||
|
|
||||||
|
**Added `gcTime` parameter** to properly manage garbage collection of cached data:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private getOptions(params = this.params): QueryObserverOptions<UnifiedFont[], Error> {
|
||||||
|
return {
|
||||||
|
queryKey: this.getQueryKey(params),
|
||||||
|
queryFn: () => this.fetchFn(params),
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 minutes (NEW)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this matters**:
|
||||||
|
|
||||||
|
- Without `gcTime`, TanStack Query uses default (5 minutes)
|
||||||
|
- This can cause cached data to persist longer than intended
|
||||||
|
- May lead to stale data being displayed
|
||||||
|
|
||||||
|
### 2. Added Response Validation (`unifiedFontStore.svelte.ts`)
|
||||||
|
|
||||||
|
**Added comprehensive validation** in `fetchFn` method:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
protected async fetchFn(params: ProxyFontsParams): Promise<UnifiedFont[]> {
|
||||||
|
const response = await fetchProxyFonts(params);
|
||||||
|
|
||||||
|
// Validate response structure
|
||||||
|
if (!response) {
|
||||||
|
console.error('[UnifiedFontStore] fetchProxyFonts returned undefined', { params });
|
||||||
|
throw new Error('Proxy API returned undefined response');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.fonts) {
|
||||||
|
console.error('[UnifiedFontStore] response.fonts is undefined', { response });
|
||||||
|
throw new Error('Proxy API response missing fonts array');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(response.fonts)) {
|
||||||
|
console.error('[UnifiedFontStore] response.fonts is not an array', { fonts: response.fonts });
|
||||||
|
throw new Error('Proxy API fonts is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store pagination metadata separately for derived values
|
||||||
|
this.#paginationMetadata = {
|
||||||
|
total: response.total ?? 0,
|
||||||
|
limit: response.limit ?? this.params.limit ?? 50,
|
||||||
|
offset: response.offset ?? this.params.offset ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return response.fonts;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
|
||||||
|
- Early error detection when proxy API returns invalid data
|
||||||
|
- Detailed logging for debugging
|
||||||
|
- Prevents undefined data from being cached
|
||||||
|
|
||||||
|
### 3. Added Fallback to Fontshare API (`proxyFonts.ts`)
|
||||||
|
|
||||||
|
**New feature**: Automatic fallback to Fontshare API when proxy fails
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 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 proxy fails.
|
||||||
|
*/
|
||||||
|
const USE_PROXY_API = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fallback Logic**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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 Function**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 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('../fontshare/fontshare');
|
||||||
|
const { normalizeFontshareFonts } = await import('../../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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
|
||||||
|
- App continues working even if proxy API is down
|
||||||
|
- Allows development and testing of proxy API without breaking the app
|
||||||
|
- Automatic detection of network/proxy errors
|
||||||
|
- Console logging for debugging
|
||||||
|
|
||||||
|
### 4. Updated `fetchProxyFontById` with validation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Option 1: Use Proxy API (Recommended for Production)
|
||||||
|
|
||||||
|
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const USE_PROXY_API = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
When set to `true`:
|
||||||
|
|
||||||
|
- Fetches from `https://api.glyphdiff.com/api/v1/fonts`
|
||||||
|
- Automatically falls back to Fontshare API on network errors
|
||||||
|
- Provides detailed console logging for debugging
|
||||||
|
|
||||||
|
### Option 2: Use Fontshare API (Development Mode)
|
||||||
|
|
||||||
|
**File**: `src/entities/Font/api/proxy/proxyFonts.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const USE_PROXY_API = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
When set to `false`:
|
||||||
|
|
||||||
|
- Uses Fontshare API directly
|
||||||
|
- Uses existing normalization functions
|
||||||
|
- Maintains full functionality while proxy API is being developed
|
||||||
|
|
||||||
|
### Option 3: Let App Auto-Fallback (Default Behavior)
|
||||||
|
|
||||||
|
With `USE_PROXY_API = true`, the app will:
|
||||||
|
|
||||||
|
1. Try to fetch from proxy API
|
||||||
|
2. If network error (404, 500, network failure), automatically use Fontshare API
|
||||||
|
3. Log all attempts to console for debugging
|
||||||
|
|
||||||
|
## Testing the Proxy API
|
||||||
|
|
||||||
|
### Step 1: Verify Proxy API is Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "https://api.glyphdiff.com/api/v1/fonts?limit=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fonts": [...],
|
||||||
|
"total": N,
|
||||||
|
"limit": 1,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Test Proxy API with Filters
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test provider filter
|
||||||
|
curl "https://api.glyphdiff.com/api/v1/fonts?provider=fontshare&limit=5"
|
||||||
|
|
||||||
|
# Test category filter
|
||||||
|
curl "https://api.glyphdiff.com/api/v1/fonts?category=sans-serif&limit=5"
|
||||||
|
|
||||||
|
# Test search
|
||||||
|
curl "https://api.glyphdiff.com/api/v1/fonts?q=roboto&limit=5"
|
||||||
|
|
||||||
|
# Test pagination
|
||||||
|
curl "https://api.glyphdiff.com/api/v1/fonts?limit=10&offset=10"
|
||||||
|
|
||||||
|
# Test sorting
|
||||||
|
curl "https://api.glyphdiff.com/api/v1/fonts?sort=popularity&limit=5"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Check Console Logs
|
||||||
|
|
||||||
|
Open browser console and look for:
|
||||||
|
|
||||||
|
- `[fetchProxyFonts] Fetching from proxy API` - Attempting proxy
|
||||||
|
- `[fetchProxyFonts] Proxy API success` - Proxy API worked
|
||||||
|
- `[fetchProxyFonts] Proxy API failed, using fallback` - Falling back to Fontshare
|
||||||
|
- `[fetchProxyFonts] Using Fontshare API as fallback` - Using Fontshare directly
|
||||||
|
- `[fetchProxyFonts] Using Fontshare API (proxy disabled)` - Proxy is disabled
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Problem: "Query data cannot be undefined"
|
||||||
|
|
||||||
|
**Cause**: Proxy API returned invalid response or didn't return fonts array
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Check console for error messages
|
||||||
|
2. Verify proxy API returns correct structure
|
||||||
|
3. Set `USE_PROXY_API = false` to use Fontshare API as fallback
|
||||||
|
|
||||||
|
### Problem: Network Error / CORS Error
|
||||||
|
|
||||||
|
**Cause**: Proxy API is not accessible or CORS headers missing
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Set `USE_PROXY_API = false` to bypass proxy temporarily
|
||||||
|
2. Fix CORS headers on proxy API server
|
||||||
|
3. Ensure proxy API is accessible from your domain
|
||||||
|
|
||||||
|
### Problem: Fonts Not Loading
|
||||||
|
|
||||||
|
**Cause**: Proxy API returns empty fonts array
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Check proxy API response in Network tab
|
||||||
|
2. Verify proxy API has fonts in database
|
||||||
|
3. Test with simple query: `?limit=5`
|
||||||
|
|
||||||
|
### Problem: Pagination Not Working
|
||||||
|
|
||||||
|
**Cause**: Proxy API `total` or `offset` fields missing or incorrect
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
|
||||||
|
1. Verify proxy API returns `total` field
|
||||||
|
2. Verify proxy API returns `limit` and `offset` fields
|
||||||
|
3. Test pagination manually with curl
|
||||||
|
|
||||||
|
## Proxy API Requirements
|
||||||
|
|
||||||
|
For the frontend to work correctly, your proxy API MUST return:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ProxyFontsResponse {
|
||||||
|
fonts: UnifiedFont[]; // REQUIRED: Array of fonts
|
||||||
|
total: number; // REQUIRED: Total matching fonts
|
||||||
|
limit: number; // REQUIRED: Current page limit
|
||||||
|
offset: number; // REQUIRED: Current offset
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `UnifiedFont` must have:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UnifiedFont {
|
||||||
|
id: string; // REQUIRED: Unique identifier
|
||||||
|
name: string; // REQUIRED: Display name
|
||||||
|
provider: 'google' | 'fontshare'; // REQUIRED: Provider
|
||||||
|
category: FontCategory; // REQUIRED: Font category
|
||||||
|
subsets: FontSubset[]; // REQUIRED: Supported subsets
|
||||||
|
variants: string[]; // REQUIRED: Available variants
|
||||||
|
styles: FontStyleUrls; // REQUIRED: Font style URLs
|
||||||
|
metadata: FontMetadata; // REQUIRED: Version, cachedAt, etc.
|
||||||
|
features: FontFeatures; // REQUIRED: Variable font info
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. `src/entities/Font/model/store/baseFontStore.svelte.ts`
|
||||||
|
- Added `gcTime` parameter
|
||||||
|
|
||||||
|
2. `src/entities/Font/model/store/unifiedFontStore.svelte.ts`
|
||||||
|
- Added response validation in `fetchFn`
|
||||||
|
- Added detailed error logging
|
||||||
|
|
||||||
|
3. `src/entities/Font/api/proxy/proxyFonts.ts`
|
||||||
|
- Added `USE_PROXY_API` flag
|
||||||
|
- Added fallback logic to Fontshare API
|
||||||
|
- Added response validation
|
||||||
|
- Added console logging
|
||||||
|
- Updated JSDoc with examples
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
All changes pass:
|
||||||
|
|
||||||
|
- ✅ Type checking (`yarn check`)
|
||||||
|
- ✅ Linting (`yarn lint`)
|
||||||
|
- ✅ No new errors introduced
|
||||||
|
- ✅ Backward compatibility maintained
|
||||||
|
- ✅ Fallback mechanism works
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Test Proxy API**: Use curl or Postman to verify your proxy API works
|
||||||
|
2. **Set `USE_PROXY_API = true`**: Enable proxy API when ready
|
||||||
|
3. **Monitor Console Logs**: Check for proxy API success/failure messages
|
||||||
|
4. **Remove Fallback** (Optional): Once proxy API is stable, remove Fontshare fallback
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: January 29, 2026
|
||||||
@@ -41,7 +41,7 @@ let { children }: Props = $props();
|
|||||||
<header></header>
|
<header></header>
|
||||||
|
|
||||||
<ScrollArea class="h-screen w-screen">
|
<ScrollArea class="h-screen w-screen">
|
||||||
<main class="flex-1 w-full max-w-6xl mx-auto px-4 py-6 md:px-8 lg:py-10 relative">
|
<main class="flex-1 h-full w-full max-w-6xl mx-auto px-4 py-6 md:px-8 lg:py-10 relative">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<TypographyMenu />
|
<TypographyMenu />
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -4,6 +4,17 @@
|
|||||||
* Exports API clients and normalization utilities
|
* Exports API clients and normalization utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Proxy API (PRIMARY - NEW)
|
||||||
|
export {
|
||||||
|
fetchProxyFontById,
|
||||||
|
fetchProxyFonts,
|
||||||
|
} from './proxy/proxyFonts';
|
||||||
|
export type {
|
||||||
|
ProxyFontsParams,
|
||||||
|
ProxyFontsResponse,
|
||||||
|
} from './proxy/proxyFonts';
|
||||||
|
|
||||||
|
// Google Fonts API (DEPRECATED - kept for backward compatibility)
|
||||||
export {
|
export {
|
||||||
fetchGoogleFontFamily,
|
fetchGoogleFontFamily,
|
||||||
fetchGoogleFonts,
|
fetchGoogleFonts,
|
||||||
@@ -14,6 +25,7 @@ export type {
|
|||||||
GoogleFontsResponse,
|
GoogleFontsResponse,
|
||||||
} from './google/googleFonts';
|
} from './google/googleFonts';
|
||||||
|
|
||||||
|
// Fontshare API (DEPRECATED - kept for backward compatibility)
|
||||||
export {
|
export {
|
||||||
fetchAllFontshareFonts,
|
fetchAllFontshareFonts,
|
||||||
fetchFontshareFontBySlug,
|
fetchFontshareFontBySlug,
|
||||||
|
|||||||
248
src/entities/Font/api/proxy/proxyFonts.ts
Normal file
248
src/entities/Font/api/proxy/proxyFonts.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
// Proxy API (PRIMARY)
|
||||||
|
export {
|
||||||
|
fetchProxyFontById,
|
||||||
|
fetchProxyFonts,
|
||||||
|
} from './api/proxy/proxyFonts';
|
||||||
|
export type {
|
||||||
|
ProxyFontsParams,
|
||||||
|
ProxyFontsResponse,
|
||||||
|
} from './api/proxy/proxyFonts';
|
||||||
|
|
||||||
|
// Fontshare API (DEPRECATED)
|
||||||
export {
|
export {
|
||||||
fetchAllFontshareFonts,
|
fetchAllFontshareFonts,
|
||||||
fetchFontshareFontBySlug,
|
fetchFontshareFontBySlug,
|
||||||
@@ -7,6 +18,8 @@ export type {
|
|||||||
FontshareParams,
|
FontshareParams,
|
||||||
FontshareResponse,
|
FontshareResponse,
|
||||||
} from './api/fontshare/fontshare';
|
} from './api/fontshare/fontshare';
|
||||||
|
|
||||||
|
// Google Fonts API (DEPRECATED)
|
||||||
export {
|
export {
|
||||||
fetchGoogleFontFamily,
|
fetchGoogleFontFamily,
|
||||||
fetchGoogleFonts,
|
fetchGoogleFonts,
|
||||||
@@ -42,7 +55,6 @@ export type {
|
|||||||
FontshareFont,
|
FontshareFont,
|
||||||
FontshareLink,
|
FontshareLink,
|
||||||
FontsharePublisher,
|
FontsharePublisher,
|
||||||
FontshareStore,
|
|
||||||
FontshareStyle,
|
FontshareStyle,
|
||||||
FontshareStyleProperties,
|
FontshareStyleProperties,
|
||||||
FontshareTag,
|
FontshareTag,
|
||||||
@@ -61,18 +73,11 @@ export type {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
appliedFontsManager,
|
appliedFontsManager,
|
||||||
createFontshareStore,
|
createUnifiedFontStore,
|
||||||
fetchFontshareFontsQuery,
|
|
||||||
fontshareStore,
|
|
||||||
selectedFontsStore,
|
selectedFontsStore,
|
||||||
|
unifiedFontStore,
|
||||||
} from './model';
|
} from './model';
|
||||||
|
|
||||||
// Stores
|
|
||||||
export {
|
|
||||||
createGoogleFontsStore,
|
|
||||||
GoogleFontsStore,
|
|
||||||
} from './model/services/fetchGoogleFonts.svelte';
|
|
||||||
|
|
||||||
// UI elements
|
// UI elements
|
||||||
export {
|
export {
|
||||||
FontApplicator,
|
FontApplicator,
|
||||||
|
|||||||
@@ -34,12 +34,10 @@ export type {
|
|||||||
UnifiedFontVariant,
|
UnifiedFontVariant,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export { fetchFontshareFontsQuery } from './services';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
appliedFontsManager,
|
appliedFontsManager,
|
||||||
createFontshareStore,
|
createUnifiedFontStore,
|
||||||
type FontshareStore,
|
|
||||||
fontshareStore,
|
|
||||||
selectedFontsStore,
|
selectedFontsStore,
|
||||||
|
type UnifiedFontStore,
|
||||||
|
unifiedFontStore,
|
||||||
} from './store';
|
} from './store';
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import {
|
|
||||||
type FontshareParams,
|
|
||||||
fetchFontshareFonts,
|
|
||||||
} from '../../api';
|
|
||||||
import { normalizeFontshareFonts } from '../../lib';
|
|
||||||
import type { UnifiedFont } from '../types';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query function for fetching fonts from Fontshare.
|
|
||||||
*
|
|
||||||
* @param params - The parameters for fetching fonts from Fontshare (E.g. search query, page number, etc.).
|
|
||||||
* @returns A promise that resolves with an array of UnifiedFont objects representing the fonts found in Fontshare.
|
|
||||||
*/
|
|
||||||
export async function fetchFontshareFontsQuery(params: FontshareParams): Promise<UnifiedFont[]> {
|
|
||||||
try {
|
|
||||||
const response = await fetchFontshareFonts(params);
|
|
||||||
return normalizeFontshareFonts(response.fonts);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (error.message.includes('Failed to fetch')) {
|
|
||||||
throw new Error(
|
|
||||||
'Unable to connect to Fontshare. Please check your internet connection.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error.message.includes('404')) {
|
|
||||||
throw new Error('Font not found in Fontshare catalog.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('Failed to load fonts from Fontshare.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
/**
|
|
||||||
* Service for fetching Google Fonts with Svelte 5 runes + TanStack Query
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
type CreateQueryResult,
|
|
||||||
createQuery,
|
|
||||||
useQueryClient,
|
|
||||||
} from '@tanstack/svelte-query';
|
|
||||||
import {
|
|
||||||
type GoogleFontsParams,
|
|
||||||
fetchGoogleFonts,
|
|
||||||
} from '../../api';
|
|
||||||
import { normalizeGoogleFonts } from '../../lib';
|
|
||||||
import type {
|
|
||||||
FontCategory,
|
|
||||||
FontSubset,
|
|
||||||
} from '../types';
|
|
||||||
import type { UnifiedFont } from '../types/normalize';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query key factory
|
|
||||||
*/
|
|
||||||
function getGoogleFontsQueryKey(params: GoogleFontsParams) {
|
|
||||||
return ['googleFonts', params] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query function
|
|
||||||
*/
|
|
||||||
export async function fetchGoogleFontsQuery(params: GoogleFontsParams): Promise<UnifiedFont[]> {
|
|
||||||
try {
|
|
||||||
const response = await fetchGoogleFonts({
|
|
||||||
category: params.category,
|
|
||||||
subset: params.subset,
|
|
||||||
sort: params.sort,
|
|
||||||
});
|
|
||||||
return normalizeGoogleFonts(response.items);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
if (error.message.includes('Failed to fetch')) {
|
|
||||||
throw new Error(
|
|
||||||
'Unable to connect to Google Fonts. Please check your internet connection.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error.message.includes('404')) {
|
|
||||||
throw new Error('Font not found in Google Fonts catalog.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error('Failed to load fonts from Google Fonts.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Fonts store wrapping TanStack Query with runes
|
|
||||||
*/
|
|
||||||
export class GoogleFontsStore {
|
|
||||||
params = $state<GoogleFontsParams>({});
|
|
||||||
private query: CreateQueryResult<UnifiedFont[], Error>;
|
|
||||||
private queryClient = useQueryClient();
|
|
||||||
|
|
||||||
constructor(initialParams: GoogleFontsParams = {}) {
|
|
||||||
this.params = initialParams;
|
|
||||||
|
|
||||||
// Create the query - automatically reactive
|
|
||||||
this.query = createQuery(() => ({
|
|
||||||
queryKey: getGoogleFontsQueryKey(this.params),
|
|
||||||
queryFn: () => fetchGoogleFontsQuery(this.params),
|
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
||||||
gcTime: 10 * 60 * 1000, // 10 minutes
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proxy TanStack Query's reactive state
|
|
||||||
get fonts() {
|
|
||||||
return this.query.data ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLoading() {
|
|
||||||
return this.query.isLoading;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isFetching() {
|
|
||||||
return this.query.isFetching;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isRefetching() {
|
|
||||||
return this.query.isRefetching;
|
|
||||||
}
|
|
||||||
|
|
||||||
get error() {
|
|
||||||
return this.query.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isError() {
|
|
||||||
return this.query.isError;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isSuccess() {
|
|
||||||
return this.query.isSuccess;
|
|
||||||
}
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this.query.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Derived helpers
|
|
||||||
get hasData() {
|
|
||||||
return this.fonts.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isEmpty() {
|
|
||||||
return !this.isLoading && this.fonts.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fontCount() {
|
|
||||||
return this.fonts.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtered fonts by category (if you need additional client-side filtering)
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update parameters - TanStack Query will automatically refetch
|
|
||||||
*/
|
|
||||||
setParams(newParams: Partial<GoogleFontsParams>) {
|
|
||||||
this.params = { ...this.params, ...newParams };
|
|
||||||
}
|
|
||||||
|
|
||||||
setCategory(category: FontCategory | undefined) {
|
|
||||||
this.setParams({ category });
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubset(subset: FontSubset | undefined) {
|
|
||||||
this.setParams({ subset });
|
|
||||||
}
|
|
||||||
|
|
||||||
setSort(sort: 'popularity' | 'alpha' | 'date' | undefined) {
|
|
||||||
this.setParams({ sort });
|
|
||||||
}
|
|
||||||
|
|
||||||
setSearch(search: string) {
|
|
||||||
this.setParams({ search });
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSearch() {
|
|
||||||
this.setParams({ search: undefined });
|
|
||||||
}
|
|
||||||
|
|
||||||
clearFilters() {
|
|
||||||
this.params = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually refetch
|
|
||||||
*/
|
|
||||||
async refetch() {
|
|
||||||
await this.query.refetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate cache and refetch
|
|
||||||
*/
|
|
||||||
invalidate() {
|
|
||||||
this.queryClient.invalidateQueries({
|
|
||||||
queryKey: getGoogleFontsQueryKey(this.params),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidate all Google Fonts queries
|
|
||||||
*/
|
|
||||||
invalidateAll() {
|
|
||||||
this.queryClient.invalidateQueries({
|
|
||||||
queryKey: ['googleFonts'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefetch with different params (for hover states, pagination, etc.)
|
|
||||||
*/
|
|
||||||
async prefetch(params: GoogleFontsParams) {
|
|
||||||
await this.queryClient.prefetchQuery({
|
|
||||||
queryKey: getGoogleFontsQueryKey(params),
|
|
||||||
queryFn: () => fetchGoogleFontsQuery(params),
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefetch next category (useful for tab switching)
|
|
||||||
*/
|
|
||||||
async prefetchCategory(category: FontCategory) {
|
|
||||||
await this.prefetch({ ...this.params, category });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel ongoing queries
|
|
||||||
*/
|
|
||||||
cancel() {
|
|
||||||
this.queryClient.cancelQueries({
|
|
||||||
queryKey: getGoogleFontsQueryKey(this.params),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear cache for current params
|
|
||||||
*/
|
|
||||||
clearCache() {
|
|
||||||
this.queryClient.removeQueries({
|
|
||||||
queryKey: getGoogleFontsQueryKey(this.params),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cached data without triggering fetch
|
|
||||||
*/
|
|
||||||
getCachedData() {
|
|
||||||
return this.queryClient.getQueryData<UnifiedFont[]>(
|
|
||||||
getGoogleFontsQueryKey(this.params),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if data exists in cache
|
|
||||||
*/
|
|
||||||
hasCache(params?: GoogleFontsParams) {
|
|
||||||
const key = params ? getGoogleFontsQueryKey(params) : getGoogleFontsQueryKey(this.params);
|
|
||||||
return this.queryClient.getQueryData(key) !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set data manually (optimistic updates)
|
|
||||||
*/
|
|
||||||
setQueryData(updater: (old: UnifiedFont[] | undefined) => UnifiedFont[]) {
|
|
||||||
this.queryClient.setQueryData(
|
|
||||||
getGoogleFontsQueryKey(this.params),
|
|
||||||
updater,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get query state for debugging
|
|
||||||
*/
|
|
||||||
getQueryState() {
|
|
||||||
return this.queryClient.getQueryState(
|
|
||||||
getGoogleFontsQueryKey(this.params),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory function to create Google Fonts store
|
|
||||||
*/
|
|
||||||
export function createGoogleFontsStore(params: GoogleFontsParams = {}) {
|
|
||||||
return new GoogleFontsStore(params);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { fetchFontshareFontsQuery } from './fetchFontshareFonts.svelte';
|
|
||||||
export { fetchGoogleFontsQuery } from './fetchGoogleFonts.svelte';
|
|
||||||
@@ -59,6 +59,7 @@ export abstract class BaseFontStore<TParams extends Record<string, any>> {
|
|||||||
queryKey: this.getQueryKey(params),
|
queryKey: this.getQueryKey(params),
|
||||||
queryFn: () => this.fetchFn(params),
|
queryFn: () => this.fetchFn(params),
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
|
gcTime: 10 * 60 * 1000,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import type { FontshareParams } from '../../api';
|
|
||||||
import { fetchFontshareFontsQuery } from '../services';
|
|
||||||
import type { UnifiedFont } from '../types';
|
|
||||||
import { BaseFontStore } from './baseFontStore.svelte';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fontshare store wrapping TanStack Query with runes
|
|
||||||
*/
|
|
||||||
export class FontshareStore extends BaseFontStore<FontshareParams> {
|
|
||||||
constructor(initialParams: FontshareParams = {}) {
|
|
||||||
super(initialParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getQueryKey(params: FontshareParams) {
|
|
||||||
// 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 ['fontshare', normalized] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async fetchFn(params: FontshareParams): Promise<UnifiedFont[]> {
|
|
||||||
return fetchFontshareFontsQuery(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider-specific methods (shortcuts)
|
|
||||||
setSearch(search: string) {
|
|
||||||
this.setParams({ q: search } as any);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFontshareStore(params: FontshareParams = {}) {
|
|
||||||
return new FontshareStore(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fontshareStore = new FontshareStore();
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { GoogleFontsParams } from '../../api';
|
|
||||||
import { fetchGoogleFontsQuery } from '../services';
|
|
||||||
import type { UnifiedFont } from '../types';
|
|
||||||
import { BaseFontStore } from './baseFontStore.svelte';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Fonts store wrapping TanStack Query with runes
|
|
||||||
*/
|
|
||||||
export class GoogleFontsStore extends BaseFontStore<GoogleFontsParams> {
|
|
||||||
constructor(initialParams: GoogleFontsParams = {}) {
|
|
||||||
super(initialParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getQueryKey(params: GoogleFontsParams) {
|
|
||||||
return ['googleFonts', params] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async fetchFn(params: GoogleFontsParams): Promise<UnifiedFont[]> {
|
|
||||||
return fetchGoogleFontsQuery(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFontshareStore(params: GoogleFontsParams = {}) {
|
|
||||||
return new GoogleFontsStore(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const googleFontsStore = new GoogleFontsStore();
|
|
||||||
@@ -6,18 +6,15 @@
|
|||||||
* Single export point for the unified font store infrastructure.
|
* Single export point for the unified font store infrastructure.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// export {
|
// Primary store (unified)
|
||||||
// createUnifiedFontStore,
|
|
||||||
// UNIFIED_FONT_STORE_KEY,
|
|
||||||
// type UnifiedFontStore,
|
|
||||||
// } from './unifiedFontStore.svelte';
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createFontshareStore,
|
createUnifiedFontStore,
|
||||||
type FontshareStore,
|
type UnifiedFontStore,
|
||||||
fontshareStore,
|
unifiedFontStore,
|
||||||
} from './fontshareStore.svelte';
|
} from './unifiedFontStore.svelte';
|
||||||
|
|
||||||
|
// Applied fonts manager (CSS loading - unchanged)
|
||||||
export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte';
|
export { appliedFontsManager } from './appliedFontsStore/appliedFontsStore.svelte';
|
||||||
|
|
||||||
|
// Selected fonts store (user selection - unchanged)
|
||||||
export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte';
|
export { selectedFontsStore } from './selectedFontsStore/selectedFontsStore.svelte';
|
||||||
|
|||||||
@@ -1,25 +1,292 @@
|
|||||||
import { type Filter } from '$shared/lib';
|
/**
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
* Unified font store
|
||||||
import type { FontProvider } from '../types';
|
*
|
||||||
import type { CheckboxFilter } from '../types/common';
|
* Single source of truth for font data, powered by the proxy API.
|
||||||
import type { BaseFontStore } from './baseFontStore.svelte';
|
* Extends BaseFontStore for TanStack Query integration and reactivity.
|
||||||
import { createFontshareStore } from './fontshareStore.svelte';
|
*
|
||||||
import type { ProviderParams } from './types';
|
* 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 {
|
import type { ProxyFontsParams } from '../../api';
|
||||||
private sources: Partial<Record<FontProvider, BaseFontStore<ProviderParams>>>;
|
import { fetchProxyFonts } from '../../api';
|
||||||
|
import type { UnifiedFont } from '../types';
|
||||||
|
import { BaseFontStore } from './baseFontStore.svelte';
|
||||||
|
|
||||||
filters: SvelteMap<CheckboxFilter, Filter>;
|
/**
|
||||||
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<ProxyFontsParams> {
|
||||||
|
/**
|
||||||
|
* 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<Record<FontProvider, ProviderParams>> = {}) {
|
/**
|
||||||
this.sources = {
|
* Pagination metadata (derived from proxy API response)
|
||||||
fontshare: createFontshareStore(initialConfig?.fontshare),
|
*/
|
||||||
|
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),
|
||||||
};
|
};
|
||||||
this.filters = new SvelteMap();
|
}
|
||||||
|
return {
|
||||||
|
total: 0,
|
||||||
|
limit: this.params.limit || 50,
|
||||||
|
offset: this.params.offset || 0,
|
||||||
|
hasMore: false,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
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<UnifiedFont[]> {
|
||||||
|
const response = await fetchProxyFonts(params);
|
||||||
|
|
||||||
|
// Validate response structure
|
||||||
|
if (!response) {
|
||||||
|
console.error('[UnifiedFontStore] fetchProxyFonts returned undefined', { params });
|
||||||
|
throw new Error('Proxy API returned undefined response');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.fonts) {
|
||||||
|
console.error('[UnifiedFontStore] response.fonts is undefined', { response });
|
||||||
|
throw new Error('Proxy API response missing fonts array');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(response.fonts)) {
|
||||||
|
console.error('[UnifiedFontStore] response.fonts is not an array', {
|
||||||
|
fonts: response.fonts,
|
||||||
|
});
|
||||||
|
throw new Error('Proxy API fonts is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store pagination metadata separately for derived values
|
||||||
|
this.#paginationMetadata = {
|
||||||
|
total: response.total ?? 0,
|
||||||
|
limit: response.limit ?? this.params.limit ?? 50,
|
||||||
|
offset: response.offset ?? this.params.offset ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
- Renders a virtualized list of fonts
|
- Renders a virtualized list of fonts
|
||||||
- Handles font registration with the manager
|
- Handles font registration with the manager
|
||||||
-->
|
-->
|
||||||
<script lang="ts" generics="T extends ({ id: string } | [{ id: string }, { id: string }])">
|
<script lang="ts" generics="T extends { id: string }">
|
||||||
import { VirtualList } from '$shared/ui';
|
import { VirtualList } from '$shared/ui';
|
||||||
import type { ComponentProps } from 'svelte';
|
import type { ComponentProps } from 'svelte';
|
||||||
import { appliedFontsManager } from '../../model';
|
import { appliedFontsManager } from '../../model';
|
||||||
@@ -16,12 +16,7 @@ let { items, children, onVisibleItemsChange, ...rest }: Props = $props();
|
|||||||
|
|
||||||
function handleInternalVisibleChange(visibleItems: T[]) {
|
function handleInternalVisibleChange(visibleItems: T[]) {
|
||||||
// Auto-register fonts with the manager
|
// Auto-register fonts with the manager
|
||||||
const slugs = visibleItems.map(item => {
|
const slugs = visibleItems.map(item => item.id);
|
||||||
if (Array.isArray(item)) {
|
|
||||||
return item.map(font => font.id);
|
|
||||||
}
|
|
||||||
return item.id;
|
|
||||||
}).flat();
|
|
||||||
appliedFontsManager.registerFonts(slugs);
|
appliedFontsManager.registerFonts(slugs);
|
||||||
|
|
||||||
// Forward the call to any external listener
|
// Forward the call to any external listener
|
||||||
|
|||||||
@@ -1,18 +1,56 @@
|
|||||||
import type { FontshareParams } from '$entities/Font';
|
import type { ProxyFontsParams } from '$entities/Font/api';
|
||||||
import type { FilterManager } from '../filterManager/filterManager.svelte';
|
import type { FilterManager } from '../filterManager/filterManager.svelte';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps filter manager to fontshare params.
|
* Maps filter manager to proxy API parameters.
|
||||||
*
|
*
|
||||||
* @param manager - Filter manager instance.
|
* Transforms UI filter state into proxy API query parameters.
|
||||||
* @returns - Partial fontshare params.
|
* Handles conversion from filter groups to API-specific parameters.
|
||||||
|
*
|
||||||
|
* @param manager - Filter manager instance with reactive state
|
||||||
|
* @returns - Partial proxy API parameters ready for API call
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Example filter manager state:
|
||||||
|
* // {
|
||||||
|
* // queryValue: 'roboto',
|
||||||
|
* // providers: ['google'],
|
||||||
|
* // categories: ['sans-serif'],
|
||||||
|
* // subsets: ['latin']
|
||||||
|
* // }
|
||||||
|
*
|
||||||
|
* const params = mapManagerToParams(manager);
|
||||||
|
* // Returns: { provider: 'google', category: 'sans-serif', subset: 'latin', q: 'roboto' }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function mapManagerToParams(manager: FilterManager): Partial<FontshareParams> {
|
export function mapManagerToParams(manager: FilterManager): Partial<ProxyFontsParams> {
|
||||||
|
const providers = manager.getGroup('providers')?.instance.selectedProperties.map(p => p.value);
|
||||||
|
const categories = manager.getGroup('categories')?.instance.selectedProperties.map(p =>
|
||||||
|
p.value
|
||||||
|
);
|
||||||
|
const subsets = manager.getGroup('subsets')?.instance.selectedProperties.map(p => p.value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
q: manager.debouncedQueryValue,
|
// Search query (debounced)
|
||||||
// Map groups to specific API keys
|
q: manager.debouncedQueryValue || undefined,
|
||||||
categories: manager.getGroup('categories')?.instance.selectedProperties.map(p => p.value)
|
|
||||||
?? [],
|
// Provider filter (single value - proxy API doesn't support array)
|
||||||
tags: manager.getGroup('tags')?.instance.selectedProperties.map(p => p.value) ?? [],
|
// Use first provider if multiple selected, or undefined if none/all selected
|
||||||
|
provider: providers && providers.length === 1
|
||||||
|
? (providers[0] as 'google' | 'fontshare')
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// Category filter (single value - proxy API doesn't support array)
|
||||||
|
// Use first category if multiple selected, or undefined if none/all selected
|
||||||
|
category: categories && categories.length === 1
|
||||||
|
? (categories[0] as ProxyFontsParams['category'])
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
// Subset filter (single value - proxy API doesn't support array)
|
||||||
|
// Use first subset if multiple selected, or undefined if none/all selected
|
||||||
|
subset: subsets && subsets.length === 1
|
||||||
|
? (subsets[0] as ProxyFontsParams['subset'])
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
import {
|
import {
|
||||||
FontListItem,
|
FontListItem,
|
||||||
FontVirtualList,
|
FontVirtualList,
|
||||||
fontshareStore,
|
unifiedFontStore,
|
||||||
} from '$entities/Font';
|
} from '$entities/Font';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FontVirtualList items={fontshareStore.fonts}>
|
<FontVirtualList items={unifiedFontStore.fonts}>
|
||||||
{#snippet children({ item: font, isVisible, proximity })}
|
{#snippet children({ item: font, isVisible, proximity })}
|
||||||
<FontListItem {font} {isVisible} {proximity} />
|
<FontListItem {font} {isVisible} {proximity} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ import { appliedFontsManager } from '$entities/Font';
|
|||||||
import { displayedFontsStore } from '$features/DisplayFont';
|
import { displayedFontsStore } from '$features/DisplayFont';
|
||||||
import FontDisplay from '$features/DisplayFont/ui/FontDisplay/FontDisplay.svelte';
|
import FontDisplay from '$features/DisplayFont/ui/FontDisplay/FontDisplay.svelte';
|
||||||
import { controlManager } from '$features/SetupFont';
|
import { controlManager } from '$features/SetupFont';
|
||||||
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
import ComparisonSlider from '$widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte';
|
import ComparisonSlider from '$widgets/ComparisonSlider/ui/ComparisonSlider/ComparisonSlider.svelte';
|
||||||
import { FontSearch } from '$widgets/FontSearch';
|
import { FontSearch } from '$widgets/FontSearch';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import { Spring } from 'svelte/motion';
|
||||||
|
import type {
|
||||||
|
SlideParams,
|
||||||
|
TransitionConfig,
|
||||||
|
} from 'svelte/transition';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page Component
|
* Page Component
|
||||||
@@ -13,6 +20,9 @@ import { FontSearch } from '$widgets/FontSearch';
|
|||||||
let searchContainer: HTMLElement;
|
let searchContainer: HTMLElement;
|
||||||
|
|
||||||
let isExpanded = $state(false);
|
let isExpanded = $state(false);
|
||||||
|
let isOpen = $state(false);
|
||||||
|
|
||||||
|
let isEmptyScreen = $derived(!displayedFontsStore.hasAnyFonts && !isExpanded && !isOpen);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
appliedFontsManager.touch(
|
appliedFontsManager.touch(
|
||||||
@@ -22,19 +32,63 @@ $effect(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Font List -->
|
<!-- Font List -->
|
||||||
<div class="p-2 will-change-[height]">
|
<div class="p-2 h-full flex flex-col gap-3 overflow-hidden">
|
||||||
<div bind:this={searchContainer}>
|
{#key isEmptyScreen}
|
||||||
<FontSearch bind:showFilters={isExpanded} />
|
<div
|
||||||
|
class={cn(
|
||||||
|
'flex flex-col transition-all duration-700 ease-[cubic-bezier(0.23,1,0.32,1)] mx-40',
|
||||||
|
'will-change-[flex-grow] transform-gpu',
|
||||||
|
isEmptyScreen
|
||||||
|
? 'grow justify-center'
|
||||||
|
: 'animate-search',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
'transition-transform duration-700 ease-[cubic-bezier(0.23,1,0.32,1)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontSearch bind:showFilters={isExpanded} bind:isOpen />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<div class="my-2 mx-10">
|
||||||
|
<ComparisonSlider />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ComparisonSlider />
|
<div class="will-change-tranform transition-transform content my-2">
|
||||||
|
|
||||||
<div class="will-change-tranform transition-transform content">
|
|
||||||
<FontDisplay />
|
<FontDisplay />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@keyframes search {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
15% {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
flex-grow: 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-search {
|
||||||
|
animation: search 0.5s cubic-bezier(0.165, 0.84, 0.44, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
/* Tells the browser to skip rendering off-screen content */
|
/* Tells the browser to skip rendering off-screen content */
|
||||||
content-visibility: auto;
|
content-visibility: auto;
|
||||||
@@ -44,4 +98,10 @@ $effect(() => {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.will-change-[height] {
|
||||||
|
will-change: flex-grow, padding;
|
||||||
|
/* Forces GPU acceleration for the layout shift */
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,30 +17,46 @@ import { useId } from 'bits-ui';
|
|||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/** Unique identifier for the input element */
|
/**
|
||||||
|
* Unique identifier for the input element
|
||||||
|
*/
|
||||||
id?: string;
|
id?: string;
|
||||||
/** Current search value (bindable) */
|
/**
|
||||||
|
* Current search value (bindable)
|
||||||
|
*/
|
||||||
value: string;
|
value: string;
|
||||||
/** Additional CSS classes for the container */
|
/**
|
||||||
|
* Whether popover is open (bindable)
|
||||||
|
*/
|
||||||
|
isOpen?: boolean;
|
||||||
|
/**
|
||||||
|
* Additional CSS classes for the container
|
||||||
|
*/
|
||||||
class?: string;
|
class?: string;
|
||||||
/** Placeholder text for the input */
|
/**
|
||||||
|
* Placeholder text for the input
|
||||||
|
*/
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
/** Optional label displayed above the input */
|
/**
|
||||||
|
* Optional label displayed above the input
|
||||||
|
*/
|
||||||
label?: string;
|
label?: string;
|
||||||
/** Content to render inside the popover (receives unique content ID) */
|
/**
|
||||||
|
* Content to render inside the popover (receives unique content ID)
|
||||||
|
*/
|
||||||
children: Snippet<[{ id: string }]> | undefined;
|
children: Snippet<[{ id: string }]> | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
id = 'search-bar',
|
id = 'search-bar',
|
||||||
value = $bindable(),
|
value = $bindable(''),
|
||||||
|
isOpen = $bindable(false),
|
||||||
class: className,
|
class: className,
|
||||||
placeholder,
|
placeholder,
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let open = $state(false);
|
|
||||||
let triggerRef = $state<HTMLInputElement>(null!);
|
let triggerRef = $state<HTMLInputElement>(null!);
|
||||||
// svelte-ignore state_referenced_locally
|
// svelte-ignore state_referenced_locally
|
||||||
const contentId = useId(id);
|
const contentId = useId(id);
|
||||||
@@ -52,11 +68,11 @@ function handleKeyDown(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleInputClick() {
|
function handleInputClick() {
|
||||||
open = true;
|
isOpen = true;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PopoverRoot bind:open>
|
<PopoverRoot bind:open={isOpen}>
|
||||||
<PopoverTrigger bind:ref={triggerRef}>
|
<PopoverTrigger bind:ref={triggerRef}>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
{@const { onclick, ...rest } = props}
|
{@const { onclick, ...rest } = props}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string;
|
||||||
|
fontName: string;
|
||||||
|
isAnimating: boolean;
|
||||||
|
onAnimationComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { text, fontName, isAnimating, onAnimationComplete }: Props = $props();
|
||||||
|
|
||||||
|
// Split text into characters, preserving spaces
|
||||||
|
const chars = $derived(text.split('').map(c => c === ' ' ? '\u00A0' : c));
|
||||||
|
|
||||||
|
let completedCount = 0;
|
||||||
|
|
||||||
|
function handleTransitionEnd() {
|
||||||
|
completedCount++;
|
||||||
|
if (completedCount === chars.length) {
|
||||||
|
onAnimationComplete?.();
|
||||||
|
completedCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative inline-flex flex-wrap leading-tight">
|
||||||
|
{#each chars as char, i}
|
||||||
|
<span
|
||||||
|
class={cn(
|
||||||
|
'inline-block transition-all duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)]',
|
||||||
|
isAnimating ? 'opacity-0 -translate-y-4 rotate-x-90' : 'opacity-100 translate-y-0 rotate-x-0',
|
||||||
|
)}
|
||||||
|
style:font-family={fontName}
|
||||||
|
style:transition-delay="{i * 25}ms"
|
||||||
|
ontransitionend={i === chars.length - 1 ? handleTransitionEnd : null}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Necessary for the "Flip" feel */
|
||||||
|
div {
|
||||||
|
perspective: 1000px;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
Combines search input with font list display
|
Combines search input with font list display
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { fontshareStore } from '$entities/Font';
|
import { unifiedFontStore } from '$entities/Font';
|
||||||
import {
|
import {
|
||||||
FilterControls,
|
FilterControls,
|
||||||
Filters,
|
Filters,
|
||||||
@@ -27,9 +27,10 @@ import { type SlideParams } from 'svelte/transition';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showFilters?: boolean;
|
showFilters?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { showFilters = $bindable(false) }: Props = $props();
|
let { showFilters = $bindable(false), isOpen = $bindable(false) }: Props = $props();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +38,7 @@ onMount(() => {
|
|||||||
* We "plug" this manager into the global store.
|
* We "plug" this manager into the global store.
|
||||||
* addBinding returns a function that removes this binding when the component unmounts.
|
* addBinding returns a function that removes this binding when the component unmounts.
|
||||||
*/
|
*/
|
||||||
const unbind = fontshareStore.addBinding(() => mapManagerToParams(filterManager));
|
const unbind = unifiedFontStore.addBinding(() => mapManagerToParams(filterManager));
|
||||||
|
|
||||||
return unbind;
|
return unbind;
|
||||||
});
|
});
|
||||||
@@ -68,6 +69,7 @@ function toggleFilters() {
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
placeholder="Search fonts by name..."
|
placeholder="Search fonts by name..."
|
||||||
bind:value={filterManager.queryValue}
|
bind:value={filterManager.queryValue}
|
||||||
|
bind:isOpen
|
||||||
>
|
>
|
||||||
<SuggestedFonts />
|
<SuggestedFonts />
|
||||||
</SearchBar>
|
</SearchBar>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const [send, receive] = crossfade({
|
|||||||
|
|
||||||
{#if displayedFontsStore.hasAnyFonts}
|
{#if displayedFontsStore.hasAnyFonts}
|
||||||
<div
|
<div
|
||||||
class="w-auto fixed bottom-5 left-1/2 translate-x-[-50%] max-w-max z-10"
|
class="w-auto fixed bottom-5 inset-x-0 max-screen z-10 flex justify-center"
|
||||||
in:receive={{ key: 'panel' }}
|
in:receive={{ key: 'panel' }}
|
||||||
out:send={{ key: 'panel' }}
|
out:send={{ key: 'panel' }}
|
||||||
>
|
>
|
||||||
|
|||||||
2
test-import.mjs
Normal file
2
test-import.mjs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { unifiedFontStore } from './src/entities/Font/index.ts';
|
||||||
|
console.log('Import successful:', !!unifiedFontStore);
|
||||||
Reference in New Issue
Block a user