feat(quick-260921-pi9): Bilderrahmen-Widget - Diashow mit Grossansicht, Bildverwaltung in den Einstellungen
- picture-frame-config.ts: Eintragstyp als Vereinigung (upload | url) in EINER geordneten Liste, resolvePictureFrameConfig laesst alles ausser https weg (T-PI9-07), Intervall 0 oder 5..3600 s, pickNextIndex (Zufall nie dasselbe) - dashboard-images-api.ts: Upload als FormData-Feld image ohne eigenen Content-Type, 413 -> deutsche Meldung, Proxy-Pfad fuer <img src> - PictureFrameWidget: Leerzustand, <img referrerPolicy=no-referrer> (Browser laedt Fremdbilder, Server nie), object-contain/cover, Unterschrift-Streifen, Wechsel per Timer mit Raeumung, kaputte Bilder verlassen den Umlauf, Grossansicht nur ausserhalb des Bearbeitungsmodus mit Fokus-Rueckgabe und Pause des Wechsels; im Bearbeitungsmodus kein Knopf (Karte bleibt Griff) - PictureFrameConfigForm im WidgetSettingsPanel: Ausschnitt, Intervall, Reihenfolge, Liste mit Vorschau/Unterschrift/Pfeilen/Entfernen (Upload wird auch serverseitig geloescht), Datei hochladen, https-Adresse hinzufuegen - Registry (4x4 min, 8x8 Vorgabe), Katalog, Seite, Uebersetzungen de/en (widgets.pictureFrame, 28 Schluessel), bestehende Tests auf acht Typen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* dashboard-images-api — Aufrufe des Bilderrahmen-Widgets gegen
|
||||
* /dashboard/images (quick-260921-pi9). Muster favorites-api.ts: Cookie
|
||||
* immer mit (`credentials: 'include'`).
|
||||
*
|
||||
* `uploadDashboardImage` schickt `multipart/form-data` mit genau dem Feld
|
||||
* `image` und OHNE eigenen Content-Type-Header — die Multipart-Grenze setzt
|
||||
* der Browser (Muster bug-report-api.ts). Die deutsche Meldung fuer „zu
|
||||
* gross“ entsteht HIER aus dem Status 413 (multer meldet englisch), die
|
||||
* Meldungen fuer 400 kommen von der API (bereits deutsch).
|
||||
*
|
||||
* `dashboardImageSrc` liefert den Proxy-Pfad fuer `<img src>`: der
|
||||
* Next-Rewrite `/api-proxy/:path*` (next.config.ts) reicht Cookies mit,
|
||||
* die Bytes kommen aus Tesseras eigenem Origin (Muster FavoriteIcon).
|
||||
*/
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||||
|
||||
export interface DashboardImageMeta {
|
||||
id: string;
|
||||
originalName: string;
|
||||
mimeType: string;
|
||||
size: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export async function fetchDashboardImages(): Promise<DashboardImageMeta[]> {
|
||||
const res = await fetch(`${API_URL}/dashboard/images`, { credentials: 'include' });
|
||||
if (!res.ok) throw new Error('Failed to fetch dashboard images');
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function uploadDashboardImage(file: File): Promise<DashboardImageMeta> {
|
||||
const body = new FormData();
|
||||
body.append('image', file, file.name);
|
||||
const res = await fetch(`${API_URL}/dashboard/images`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body,
|
||||
});
|
||||
if (res.ok) return res.json();
|
||||
|
||||
if (res.status === 413) {
|
||||
throw new Error('Die Datei ist zu groß – erlaubt sind höchstens 5 MB.');
|
||||
}
|
||||
if (res.status === 400) {
|
||||
const message = await readMessage(res);
|
||||
if (message) throw new Error(message);
|
||||
}
|
||||
throw new Error('Das Bild konnte nicht hochgeladen werden.');
|
||||
}
|
||||
|
||||
export async function deleteDashboardImage(id: string): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/dashboard/images/${encodeURIComponent(id)}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to delete dashboard image');
|
||||
}
|
||||
|
||||
export function dashboardImageSrc(id: string): string {
|
||||
return `/api-proxy/dashboard/images/${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
async function readMessage(res: Response): Promise<string | null> {
|
||||
try {
|
||||
const data: unknown = await res.json();
|
||||
if (typeof data === 'object' && data !== null && 'message' in data) {
|
||||
const message = (data as { message: unknown }).message;
|
||||
if (typeof message === 'string' && message !== '') return message;
|
||||
}
|
||||
} catch {
|
||||
// kein JSON — allgemeine Meldung
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user