feat(frontend): add API client and Svelte stores

- Create API client with auto-retry and token refresh support
- Add comprehensive API endpoints for tasks, tags, projects, sync, and auth
- Implement authStore for authentication state management
- Implement tasksStore with optimistic updates and offline queue
- Add derived stores for filtered task views (pending, completed, by project)
- Implement syncStore for managing sync state and queue
- Add client ID generation and persistence for sync tracking
This commit is contained in:
2026-01-06 15:45:13 +01:00
parent 41795d1827
commit d99e158a8c
5 changed files with 754 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
import { authStore } from '$lib/stores/auth.js';
import { get } from 'svelte/store';
/**
* @typedef {import('./types.js').APIResponse} APIResponse
* @typedef {import('./types.js').AuthTokens} AuthTokens
*/
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080';
/**
* Make authenticated API request
* @template T
* @param {string} endpoint
* @param {RequestInit} [options]
* @returns {Promise<T>}
*/
export async function apiRequest(endpoint, options = {}) {
const auth = get(authStore);
const headers = {
'Content-Type': 'application/json',
...options.headers
};
// Add auth token if available
if (auth.accessToken) {
headers['Authorization'] = `Bearer ${auth.accessToken}`;
}
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers
});
// Token expired - try refresh
if (response.status === 401 && auth.refreshToken) {
const refreshed = await refreshAccessToken(auth.refreshToken);
if (refreshed) {
// Retry with new token
headers['Authorization'] = `Bearer ${refreshed.access_token}`;
return apiRequest(endpoint, { ...options, headers });
}
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Unknown error');
}
return result.data;
} catch (error) {
console.error(`API Error [${endpoint}]:`, error);
throw error;
}
}
/**
* Refresh access token
* @param {string} refreshToken
* @returns {Promise<AuthTokens|null>}
*/
async function refreshAccessToken(refreshToken) {
try {
const response = await fetch(`${API_BASE}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (!response.ok) return null;
const result = await response.json();
if (result.success) {
// Update auth store
authStore.setTokens(result.data);
return result.data;
}
return null;
} catch {
return null;
}
}