Phase C: die letzten sechs Typen — PDF, Bild, Feedback und Datei-Upload

Damit sind alle 15 QR-Typen gebaut.

PDF und Bild brauchen eine hochgeladene Datei. Die liegt in der Datenbank,
nicht auf der Platte: damit bleibt es bei einem Volumen, das gesichert werden
muss — ein zweites wird beim Sichern zu leicht vergessen. Geprueft wird der
Dateiinhalt, nicht die Endung; eine Textdatei namens harmlos.pdf kommt nicht
durch. SVG ist bewusst nicht erlaubt, weil es Skripte enthalten darf und von
derselben Adresse wie die Anwendung ausgeliefert wuerde. Ausgeliefert wird mit
nosniff und einer CSP-Sandbox, damit auch ein PDF mit Skripten nichts anrichtet.

Dateien haengen am Code: wird er geloescht, geht sie mit. Eine ausgetauschte
oder nie gespeicherte Datei wird nach 24 Stunden weggeraeumt. Eine fremde
Datei laesst sich nicht an den eigenen Code haengen — sonst koennte man sie
einem anderen Code entziehen.

Feedback sammelt Note und Kommentar, ohne Name, IP oder Geraetekennung. Die
Auswertung mit Durchschnitt, Verteilung und Kommentaren ist aus der Codeliste
erreichbar — ein Feedback-Code ohne Auswertung waere nur die halbe Sache.
This commit is contained in:
Florian Wilde
2026-08-14 23:34:28 +02:00
parent fdf6452b35
commit e296b1e6c2
32 changed files with 1819 additions and 37 deletions
+49
View File
@@ -0,0 +1,49 @@
import { Icon } from "@/components/icons";
import { LandingShell } from "@/components/landing/shell";
import type { LandingProps } from "@/components/landing/types";
import { pdfDataSchema } from "@/lib/qr/types/pdf";
import { formatBytes } from "@/lib/uploads";
export function PdfLanding({ data }: LandingProps) {
const { fileId, filename, size, title, description } =
pdfDataSchema.parse(data);
const href = `/api/uploads/${fileId}`;
return (
<LandingShell
title={title || filename || "PDF-Dokument"}
subtitle={description || null}
>
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
<div className="flex items-center gap-4">
<span className="flex size-12 shrink-0 items-center justify-center rounded-xl bg-red-50 text-red-600">
<Icon name="document" className="size-6" />
</span>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-slate-900">{filename}</p>
<p className="text-sm text-slate-500">
PDF{size > 0 ? ` · ${formatBytes(size)}` : ""}
</p>
</div>
</div>
<div className="mt-5 grid gap-2 sm:grid-cols-2">
<a
href={href}
className="inline-flex items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
>
<Icon name="document" className="size-4" />
Dokument öffnen
</a>
<a
href={`${href}?download`}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50"
>
<Icon name="download" className="size-4" />
Herunterladen
</a>
</div>
</div>
</LandingShell>
);
}