♻️ refactor: migrate calendar, picture, nutriphi, planta, questions, skilltree to shared-api-client

- Update all web apps to use @manacore/shared-api-client
- Remove calendar's local base-client.ts (duplicate of shared package)
- Calendar: update todos.ts and birthdays.ts to use shared client
- Maintain backward compatibility with existing patterns:
  - picture: fetchApi, uploadFile, uploadFiles functions
  - nutriphi: apiClient class with throw-based errors
  - planta: fetchApi function with {data, error} format
  - questions/skilltree: apiClient with setAccessToken pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-29 14:32:47 +01:00
parent 75b5fb2fae
commit 1e5175e522
16 changed files with 354 additions and 429 deletions

View file

@ -36,6 +36,7 @@
},
"dependencies": {
"@nutriphi/shared": "workspace:*",
"@manacore/shared-api-client": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",

View file

@ -1,68 +1,65 @@
/**
* API Client for NutriPhi backend
* Uses @manacore/shared-api-client for consistent error handling
*/
import { createApiClient, type ApiResult } from '@manacore/shared-api-client';
import { authStore } from '$lib/stores/auth.svelte';
import { PUBLIC_BACKEND_URL } from '$env/static/public';
const BASE_URL = PUBLIC_BACKEND_URL || 'http://localhost:3023';
/**
* NutriPhi API client instance
* - Auto token handling via authStore.getAccessToken()
* - Consistent ApiResult<T> response format
*/
const api = createApiClient({
baseUrl: BASE_URL,
apiPrefix: '/api/v1',
getAuthToken: () => authStore.getAccessToken(),
timeout: 30000,
debug: import.meta.env.DEV,
});
/**
* Legacy ApiClient class wrapper for backward compatibility
* Maintains throw-based error handling for existing code
*/
class ApiClient {
private async getHeaders(): Promise<HeadersInit> {
const token = await authStore.getAccessToken();
return {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
};
}
async get<T>(path: string): Promise<T> {
const response = await fetch(`${BASE_URL}/api/v1${path}`, {
method: 'GET',
headers: await this.getHeaders(),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
const result = await api.get<T>(path);
if (result.error) {
throw new Error(result.error.message);
}
return response.json();
return result.data as T;
}
async post<T>(path: string, data: unknown): Promise<T> {
const response = await fetch(`${BASE_URL}/api/v1${path}`, {
method: 'POST',
headers: await this.getHeaders(),
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
const result = await api.post<T>(path, data);
if (result.error) {
throw new Error(result.error.message);
}
return response.json();
return result.data as T;
}
async patch<T>(path: string, data: unknown): Promise<T> {
const response = await fetch(`${BASE_URL}/api/v1${path}`, {
method: 'PATCH',
headers: await this.getHeaders(),
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
const result = await api.patch<T>(path, data);
if (result.error) {
throw new Error(result.error.message);
}
return response.json();
return result.data as T;
}
async delete(path: string): Promise<void> {
const response = await fetch(`${BASE_URL}/api/v1${path}`, {
method: 'DELETE',
headers: await this.getHeaders(),
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
const result = await api.delete<void>(path);
if (result.error) {
throw new Error(result.error.message);
}
}
}
export const apiClient = new ApiClient();
// Re-export types for convenience
export type { ApiResult };