/** * Favorites API client functions. * Mirrors the NestJS FavoritesController routes (08-03). * All calls use credentials: 'include' for cookie-based auth. * PUT /favorites/order (reorderFavorites) added 260917-jdd. * * 260923-lrr — eigenes Symbol hochladen, Zwischenspeicher nach Aenderung * erneuern: `FavoriteLink` traegt jetzt `uploadedIconMime`/`iconVersion` * (optional, weil Testdaten und aeltere Antworten sie nicht tragen). * `FavoriteRequestError` traegt den Grund einer abgewiesenen Symbol-Datei * (Widget bildet ihn auf eine sprachrichtige Meldung ab). 260929-lh3: die * 422-Abrufprobe fuer Logo-Adressen ist entfallen. `uploadFavoriteIcon`/`removeFavoriteIcon` sind neu, * Muster `uploadDashboardImage`/`deleteDashboardImage` (dashboard-images-api.ts). */ const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; /** Hoechstgroesse eines hochgeladenen Symbols — muss der API-Konstante entsprechen. */ export const FAVORITE_ICON_MAX_BYTES = 512 * 1024; export interface FavoriteLink { id: string; widgetId: string; title: string; url: string; iconUrl: string | null; position: number; /** 260923-lrr — Typ eines hochgeladenen eigenen Symbols, null = keins. */ uploadedIconMime?: string | null; /** 260923-lrr — Zaehler fuer die Symbol-Adresse (`?v=`), macht den 24h-Zwischenspeicher nach einer Aenderung sofort ungueltig. */ iconVersion?: number; } /** Grund einer abgewiesenen Favoriten-Anfrage (260923-lrr). */ export type FavoriteErrorReason = | 'iconTooLarge' | 'iconInvalidType' | 'iconUploadFailed'; /** * Ein abgewiesener Favoriten-Aufruf mit einem uebersetzbaren Grund * (260923-lrr) — das Widget bildet `reason` auf `t('favorites.' + reason)` * ab, damit die Meldung sprachrichtig ist statt einer festen deutschen * Zeichenkette aus dem Klienten. */ export class FavoriteRequestError extends Error { public readonly reason: FavoriteErrorReason; constructor(reason: FavoriteErrorReason) { super(reason); this.name = 'FavoriteRequestError'; this.reason = reason; } } /** * Fetch all favorite links for a specific widget instance. * The widgetId parameter scopes the query to the correct widget (Pitfall 3). */ export async function fetchFavorites(widgetId: string): Promise { const res = await fetch( `${API_URL}/favorites?widgetId=${encodeURIComponent(widgetId)}`, { credentials: 'include' }, ); if (!res.ok) throw new Error('Failed to fetch favorites'); return res.json(); } /** * Create a new favorite link. * Server-side icon discovery runs automatically if iconUrl is not provided. * 260929-lh3: eine ausdrueckliche `iconUrl` wird auch gespeichert, wenn der * Server sie nicht abrufen kann (die Kachel laedt sie dann im Browser); die * frueheren 422-Antworten gibt es nicht mehr. */ export async function createFavorite(payload: { widgetId: string; title: string; url: string; iconUrl?: string; }): Promise { const res = await fetch(`${API_URL}/favorites`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload), }); if (!res.ok) throw new Error('Failed to create favorite'); return res.json(); } /** * Update an existing favorite link. * Pass iconUrl: null to clear a stored icon. * 260929-lh3: wie `createFavorite` — keine 422-Abweisung wegen Abrufbarkeit mehr. */ export async function updateFavorite( id: string, payload: Partial<{ title: string; url: string; iconUrl: string | null; position: number }>, ): Promise { const res = await fetch(`${API_URL}/favorites/${encodeURIComponent(id)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(payload), }); if (!res.ok) throw new Error('Failed to update favorite'); return res.json(); } /** * Persist the display order for a widget's favorites (260917-jdd). * `ids` is the full id list in the desired order; the server responds with * the favorites of this widget in the new order. */ export async function reorderFavorites( widgetId: string, ids: string[], ): Promise { const res = await fetch(`${API_URL}/favorites/order`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ widgetId, ids }), }); if (!res.ok) throw new Error('Failed to reorder favorites'); return res.json(); } /** * Delete a favorite link by id. */ export async function deleteFavorite(id: string): Promise { const res = await fetch(`${API_URL}/favorites/${encodeURIComponent(id)}`, { method: 'DELETE', credentials: 'include', }); if (!res.ok) throw new Error('Failed to delete favorite'); } /** * Laedt ein eigenes Symbol fuer einen Favoriten hoch (260923-lrr). Muster * `uploadDashboardImage` (dashboard-images-api.ts): FormData ohne eigenen * Content-Type-Header — die Multipart-Grenze setzt der Browser. Die * Groessenpruefung laeuft VOR dem Netzwerkaufruf (kein Aufruf fuer eine * offensichtlich zu grosse Datei). */ export async function uploadFavoriteIcon(id: string, file: File): Promise { if (file.size > FAVORITE_ICON_MAX_BYTES) { throw new FavoriteRequestError('iconTooLarge'); } const body = new FormData(); body.append('icon', file, file.name); const res = await fetch(`${API_URL}/favorites/${encodeURIComponent(id)}/icon`, { method: 'POST', credentials: 'include', body, }); if (res.ok) return res.json(); if (res.status === 413) throw new FavoriteRequestError('iconTooLarge'); if (res.status === 400) throw new FavoriteRequestError('iconInvalidType'); throw new FavoriteRequestError('iconUploadFailed'); } /** * Entfernt ein zuvor hochgeladenes Symbol wieder (260923-lrr); die Kachel * faellt danach auf `iconUrl` bzw. automatische Erkennung zurueck. */ export async function removeFavoriteIcon(id: string): Promise { const res = await fetch(`${API_URL}/favorites/${encodeURIComponent(id)}/icon`, { method: 'DELETE', credentials: 'include', }); if (!res.ok) throw new Error('Failed to remove favorite icon'); return res.json(); }