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:
+13
-4
@@ -38,16 +38,24 @@ export function toQrCodeDto(code: QrCodeWithRelations): QrCodeDto {
|
||||
const data = asRecord(code.data);
|
||||
const qrType = getQrType(code.type);
|
||||
|
||||
const istStatisch = code.mode === "static";
|
||||
|
||||
let target = "";
|
||||
let summary = "";
|
||||
if (qrType) {
|
||||
try {
|
||||
// Ein statischer Code führt nirgendwohin — sein Inhalt steckt in ihm
|
||||
// selbst. Als „Ziel“ bleibt deshalb nichts übrig; die Zusammenfassung
|
||||
// sagt trotzdem, was drinsteht.
|
||||
// Typen mit eigener Landingpage führen auf eine Seite dieser Anwendung.
|
||||
// Die ist als „Ziel“ das Ehrlichste — und obendrein anklickbar, um sie
|
||||
// sich anzusehen.
|
||||
target = qrType.hasLandingPage
|
||||
? landingUrl(code.slug, code.domain?.hostname)
|
||||
: qrType.redirectTarget(data);
|
||||
target =
|
||||
istStatisch || !code.slug
|
||||
? ""
|
||||
: qrType.hasLandingPage
|
||||
? landingUrl(code.slug, code.domain?.hostname)
|
||||
: qrType.redirectTarget(data);
|
||||
summary = qrType.summary(data);
|
||||
} catch {
|
||||
// Datensatz passt nicht mehr zum Typ-Schema (z. B. nach einem
|
||||
@@ -60,12 +68,13 @@ export function toQrCodeDto(code: QrCodeWithRelations): QrCodeDto {
|
||||
|
||||
return {
|
||||
id: code.id,
|
||||
mode: istStatisch ? "static" : "dynamic",
|
||||
slug: code.slug,
|
||||
aliases: (code.aliases ?? []).map((alias) => alias.slug),
|
||||
type: code.type as QrTypeId,
|
||||
label: code.label,
|
||||
data,
|
||||
shortUrl: shortUrl(code.slug, code.domain?.hostname),
|
||||
shortUrl: code.slug ? shortUrl(code.slug, code.domain?.hostname) : null,
|
||||
target,
|
||||
summary,
|
||||
design: {
|
||||
|
||||
+15
-5
@@ -1,21 +1,31 @@
|
||||
import type { EcLevel } from "@/lib/qr/design";
|
||||
import type { QrTypeId } from "@/lib/qr/type-module";
|
||||
|
||||
/** Statisch oder dynamisch — bestimmt, was ein Code überhaupt kann. */
|
||||
export type QrCodeMode = "static" | "dynamic";
|
||||
|
||||
/**
|
||||
* Darstellung eines dynamischen Codes an der API-Grenze. Bewusst frei von
|
||||
* Darstellung eines gespeicherten Codes an der API-Grenze. Bewusst frei von
|
||||
* Server-Abhängigkeiten, damit Client-Komponenten den Typ verwenden können.
|
||||
*/
|
||||
export type QrCodeDto = {
|
||||
id: string;
|
||||
slug: string;
|
||||
/**
|
||||
* Ein statischer Code trägt seinen Inhalt selbst. Er ist nur abgelegt, damit
|
||||
* man ihn wiederfindet und erneut herunterladen kann — Kurz-Link, Ziel und
|
||||
* Scan-Zähler bleiben deshalb leer.
|
||||
*/
|
||||
mode: QrCodeMode;
|
||||
/** Kurz-Link. Leer bei statischen Codes. */
|
||||
slug: string | null;
|
||||
/** Frühere Kurz-Links, die weiterhin auf diesen Code führen. */
|
||||
aliases: string[];
|
||||
type: QrTypeId;
|
||||
label: string | null;
|
||||
data: Record<string, unknown>;
|
||||
/** Die Kurz-URL, die im QR-Code steckt. */
|
||||
shortUrl: string;
|
||||
/** Aktuelles Weiterleitungsziel. */
|
||||
/** Die Kurz-URL, die im QR-Code steckt. Leer bei statischen Codes. */
|
||||
shortUrl: string | null;
|
||||
/** Aktuelles Weiterleitungsziel. Leer bei statischen Codes. */
|
||||
target: string;
|
||||
/** Lesbare Kurzfassung des Ziels. */
|
||||
summary: string;
|
||||
|
||||
+28
-10
@@ -36,6 +36,8 @@ export const qrRequestSchema = z.object({
|
||||
slug: z.string().trim().optional(),
|
||||
/** Domain, unter der die Kurz-URL entsteht. */
|
||||
domainId: z.string().trim().optional(),
|
||||
/** Name des Codes — bestimmt den Dateinamen beim Herunterladen. */
|
||||
label: z.string().trim().max(120).optional(),
|
||||
});
|
||||
|
||||
export type QrRequest = z.infer<typeof qrRequestSchema>;
|
||||
@@ -137,25 +139,41 @@ export async function resolveQrRequest(
|
||||
};
|
||||
}
|
||||
|
||||
/** Dateiname für den Download, abgeleitet aus dem Inhalt des Codes. */
|
||||
/** Macht aus beliebigem Text einen brauchbaren Dateinamen-Bestandteil. */
|
||||
function alsDateiname(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replaceAll("ä", "ae")
|
||||
.replaceAll("ö", "oe")
|
||||
.replaceAll("ü", "ue")
|
||||
.replaceAll("ß", "ss")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dateiname für den Download.
|
||||
*
|
||||
* Wenn ein Name vergeben wurde, gewinnt er — genau dafür ist er da: damit man
|
||||
* die Datei später wiederfindet. Sonst wie bisher aus dem Inhalt abgeleitet.
|
||||
*/
|
||||
export function downloadFilename(
|
||||
qrType: QrTypeModule,
|
||||
data: unknown,
|
||||
extension: string,
|
||||
label?: string | null,
|
||||
): string {
|
||||
const ausName = label ? alsDateiname(label) : "";
|
||||
if (ausName) return `qr-${ausName}.${extension}`;
|
||||
|
||||
let base = "qr-code";
|
||||
try {
|
||||
const summary = qrType
|
||||
.summary(data)
|
||||
.toLowerCase()
|
||||
.replaceAll("ä", "ae")
|
||||
.replaceAll("ö", "oe")
|
||||
.replaceAll("ü", "ue")
|
||||
.replaceAll("ß", "ss")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 60);
|
||||
if (summary) base = `qr-${summary}`;
|
||||
.toLowerCase();
|
||||
const gesaeubert = alsDateiname(summary);
|
||||
if (gesaeubert) base = `qr-${gesaeubert}`;
|
||||
} catch {
|
||||
// Zusammenfassung ist optional — im Zweifel bleibt der Standardname.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user