Files
nexarch/web/mail-search/app/mail/[messageId]/ImagePreview.tsx
T
patrickandClaude Sonnet 5 f63fcb8a2d
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (pull_request) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (pull_request) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (pull_request) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (pull_request) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (pull_request) Canceled after 0s
CI / Web (dms-browser) (pull_request) Canceled after 0s
CI / Web (pdf-viewer) (pull_request) Canceled after 0s
CI / Web (shl) (pull_request) Canceled after 0s
Core-Schnittstellen-Vertragstests / contract-tests (pull_request) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (pull_request) Canceled after 0s
Mail-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
Supply-Chain-Scan (npm) / npm-audit (pull_request) Canceled after 0s
Core-Pflichttest-Gate / pflichttest-gate (pull_request) Canceled after 0s
RBAC-Rollenhierarchie-Pflichtgate / rbac-policy-invariant (pull_request) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (pull_request) Canceled after 0s
UX-03: anhang-vorschau
Frontend (web/mail-search): Anhang-Vorschau fuer PDF (Canvas-Rendering
via pdfjs-dist, gleiches Muster wie DOC-05s web/pdf-viewer/PdfViewer.tsx
-- kein aequivalentes PdfCanvasPreview im Repo gefunden, daher PdfViewer
als Vorlage uebernommen statt neu erfunden) und Bilder (<img> auf einem
authentifiziert geladenen Blob). Die Vorschauart haengt ausschliesslich
vom serverseitig per Magic-Bytes bestimmten contentType ab, nie von
einem Dateinamen (den es an der Schnittstelle gar nicht gibt); nicht
unterstuetzte Typen werden nur als Hinweistext angezeigt, nie
eingebettet. Kein iframe/<object>/dangerouslySetInnerHTML -- durch
automatisierten Sicherheitsreview-Test abgesichert. Neue lib/http.ts
(authentifizierter Zugriff ueber @nexarch/shl, identisch zu
web/pdf-viewer/lib/http.ts) und lib/attachments.ts (Anhangsliste/
-inhalt der Mail-API). Pruefprotokoll in mail/docs/UX-03-PRUEFPROTOKOLL.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-16 06:51:29 +00:00

55 lines
1.6 KiB
TypeScript

"use client";
// UX-03: Bild-Anhang-Vorschau. <img> dekodiert Bilddaten nativ im
// Browser (kein Skript-/HTML-Rendering von Fremdinhalt wie bei einem
// iframe) -- fuer Bilder reicht das, anders als bei PDF ist hier kein
// Canvas-Zwang nötig (Akzeptanzkriterium 3 betrifft ausdrücklich nur
// die PDF-Vorschau).
import { useEffect, useState } from "react";
import { apiFetch } from "../../../lib/http";
export interface ImagePreviewProps {
contentUrl: string;
alt: string;
}
export function ImagePreview({ contentUrl, alt }: ImagePreviewProps) {
const [src, setSrc] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
let objectUrl: string | null = null;
setError(null);
setSrc(null);
apiFetch(contentUrl)
.then((res) => {
if (!res.ok) throw new Error(`Bild konnte nicht geladen werden (${res.status}).`);
return res.blob();
})
.then((blob) => {
if (cancelled) return;
objectUrl = URL.createObjectURL(blob);
setSrc(objectUrl);
})
.catch((err: unknown) => {
if (cancelled) return;
setError(err instanceof Error ? err.message : "Bild konnte nicht geladen werden.");
});
return () => {
cancelled = true;
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [contentUrl]);
if (error) {
return <p role="alert">{error}</p>;
}
if (!src) {
return <p>Bild wird geladen…</p>;
}
return <img src={src} alt={alt} style={{ maxWidth: "100%" }} />;
}