Statische Codes benennen und auf Wunsch ablegen

Das Feld „Name (optional)" stand nur im Dynamisch-Block. Bei einem statischen
Code gab es damit keine Moeglichkeit, ihm einen Namen zum Wiederfinden zu
geben.

Jetzt gilt es in beiden Betriebsarten und tut zweierlei: Es bestimmt den
Dateinamen beim Herunterladen — aus „Plakat Eingang" wird
qr-plakat-eingang.png statt qr-example-com-plakat.png — und es laesst sich
damit ein statischer Code in „Meine Codes" ablegen.

Ein abgelegter statischer Code ist ein Schnappschuss, kein steuerbares Ziel.
Aenderbar ist nur der Name; Inhalt und Gestaltung bleiben, wie sie beim
Ablegen waren, denn genau das steckt im gedruckten Bild. Er bekommt keinen
Kurz-Link, ist unter /r nicht erreichbar und zaehlt keine Scans — in der
Liste steht dort „statisch" und ein Strich. Wer nach dem Ablegen den Inhalt
aendert, bekommt das Ablegen erneut angeboten; der vorherige Eintrag bleibt
als eigener Schnappschuss.

Dafuer traegt QrCode jetzt `mode`, und `slug` darf leer sein. In PostgreSQL
schliessen sich mehrere NULL in einem eindeutigen Index nicht aus, die
Bedingung „je Domain eindeutig" gilt fuer dynamische Codes also unveraendert.

Refs #2
This commit is contained in:
Florian Wilde
2026-08-20 19:33:49 +02:00
parent 46e7eb0b59
commit eeaf3820b7
15 changed files with 370 additions and 108 deletions
+49 -2
View File
@@ -28,6 +28,12 @@ const createSchema = z.object({
.trim()
.max(120, { error: "Der Name darf höchstens 120 Zeichen lang sein." })
.optional(),
/**
* Statisch oder dynamisch. Ein statischer Code wird nur abgelegt, damit man
* ihn wiederfindet — er bekommt keinen Kurz-Link und zählt keine Scans.
*/
mode: z.enum(["static", "dynamic"], { error: "Ungültiger Modus." })
.default("dynamic"),
/**
* Selbst gewählter Kurz-Link. Ohne Angabe wird einer erzeugt.
* Geprüft wird er in `describeSlugProblem` — hier nur die grobe Länge.
@@ -101,8 +107,25 @@ export async function POST(request: NextRequest): Promise<Response> {
return jsonError(validated.message);
}
// Erst hier, nach der Validierung: die Probe braucht die geprüften Daten.
assertDynamicPossible(qrType, validated.data);
const istStatisch = parsed.data.mode === "static";
if (istStatisch) {
// Ein statischer Code trägt seinen Inhalt selbst — der Typ muss das
// können, sonst gäbe es nichts zu kodieren.
if (!qrType.supportsStatic) {
return jsonError(
`Der QR-Typ „${qrType.title}“ funktioniert nur als dynamischer Code.`,
);
}
if (parsed.data.slug) {
return jsonError(
"Ein statischer Code hat keinen Kurz-Link. Sein Inhalt steckt im Code selbst.",
);
}
} else {
// Erst hier, nach der Validierung: die Probe braucht die geprüften Daten.
assertDynamicPossible(qrType, validated.data);
}
// Ein selbst gewählter Kurz-Link wird geprüft, bevor irgendetwas entsteht —
// sonst stünde am Ende ein Code mit einer Adresse, die niemand wollte.
@@ -117,6 +140,30 @@ export async function POST(request: NextRequest): Promise<Response> {
await assertSlugAvailable(domainId, gewuenscht);
}
if (istStatisch) {
const created = await prisma.qrCode.create({
data: {
mode: "static",
slug: null,
type: qrType.id,
label: label && label.length > 0 ? label : null,
data: toJsonInput(validated.data),
groupId,
domainId,
createdById: actor.kind === "user" ? actor.user.id : null,
...design,
},
include: {
group: { select: { name: true } },
domain: { select: { hostname: true } },
createdBy: { select: { name: true, email: true } },
aliases: { select: { slug: true }, orderBy: { createdAt: "desc" } },
},
});
await attachUploads(created.id, qrType.uploadIds(validated.data));
return Response.json({ code: toQrCodeDto(created) }, { status: 201 });
}
// Slug-Kollisionen sind extrem unwahrscheinlich, aber nicht unmöglich.
for (let attempt = 0; attempt < 5; attempt += 1) {
try {