b15c74632b
- API: ausdrueckliche iconUrl wird nur auf Form (http/https, <= 2048) geprueft und auch gespeichert, wenn der Server sie nicht abrufen kann; keine 422 mehr - Erkennung: Seite mit Fehlerstatus, aber HTML mit <link rel=icon>, liefert diesen Verweis (docuvita); og:image einer Fehlerseite zaehlt nicht - Kachel: Proxy -> iconUrl direkt im Browser (no-referrer, nur http/https) -> Origin-Favicon -> Buchstabe - Meldung iconUrlUnreachable (de/en) entfernt, Hinweis zum Vorrang angepasst Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
184 lines
6.1 KiB
TypeScript
184 lines
6.1 KiB
TypeScript
/**
|
|
* 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<FavoriteLink[]> {
|
|
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<FavoriteLink> {
|
|
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<FavoriteLink> {
|
|
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<FavoriteLink[]> {
|
|
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<void> {
|
|
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<FavoriteLink> {
|
|
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<FavoriteLink> {
|
|
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();
|
|
}
|