V1: Next.js-App mit QR-Typ-Registry, Website-Generator und dynamischen Codes
- Erweiterbare Registry aller 15 QR-Typen; nur "Website" ist aktiv, die
uebrigen erscheinen als Platzhalter mit Badge "Bald verfuegbar".
- defineQrType() kapselt Zod-Schema, Kodierung und Redirect-Ziel je Typ,
sodass ein V2-Typ nur aus einem Registry-Eintrag plus Formular besteht.
- Serverseitige QR-Erzeugung (PNG/SVG) mit Farben, Groesse, Fehlerkorrektur
und Rand; Live-Vorschau ueber /api/qr/preview.
- Dynamische Codes: Kurz-URL {BASE_URL}/r/{slug}, 302-Redirect mit
Scan-Zaehler, CRUD unter /api/codes.
- Prisma 7 mit pg-Driver-Adapter und Initial-Migration fuer PostgreSQL.
- Deutsche Oberflaeche mit Tailwind CSS, helles Theme, responsiv.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import type { ComponentType } from "react";
|
||||
|
||||
import { WebsiteForm } from "@/components/qr-forms/website-form";
|
||||
import type { QrFormProps } from "@/components/qr-forms/types";
|
||||
import type { QrTypeId } from "@/lib/qr/type-module";
|
||||
|
||||
/**
|
||||
* Zuordnung QR-Typ → Formular-Komponente.
|
||||
*
|
||||
* Gegenstück zur Typ-Registry in `src/lib/qr/registry.ts`: dort liegt die
|
||||
* Logik (Validierung, Kodierung), hier die Eingabemaske. Ein V2-Typ braucht
|
||||
* genau einen zusätzlichen Eintrag an beiden Stellen.
|
||||
*/
|
||||
export const qrForms: Partial<Record<QrTypeId, ComponentType<QrFormProps>>> = {
|
||||
website: WebsiteForm,
|
||||
};
|
||||
|
||||
export type { QrFormProps };
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { QrFormData } from "@/lib/qr/type-module";
|
||||
|
||||
/**
|
||||
* Einheitliche Schnittstelle aller Typ-Formulare. Das Formular kennt nur
|
||||
* seine eigenen Felder und meldet Änderungen als flaches Objekt zurück —
|
||||
* dasselbe Format, das das zugehörige Typ-Modul validiert.
|
||||
*/
|
||||
export type QrFormProps = {
|
||||
value: QrFormData;
|
||||
onChange: (value: QrFormData) => void;
|
||||
/** Validierungsmeldung zum aktuellen Inhalt. */
|
||||
error?: string | null;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { Field, TextInput } from "@/components/ui";
|
||||
import type { QrFormProps } from "@/components/qr-forms/types";
|
||||
|
||||
export function WebsiteForm({ value, onChange, error, disabled }: QrFormProps) {
|
||||
const url = typeof value.url === "string" ? value.url : "";
|
||||
// Häufigster Eingabefehler: die Adresse wird ohne Schema getippt.
|
||||
const missingScheme = url.trim().length > 0 && !/^[a-z][a-z0-9+.-]*:/i.test(url);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Field
|
||||
label="Website-Adresse"
|
||||
htmlFor="website-url"
|
||||
error={error}
|
||||
hint="Die vollständige Adresse inklusive https://"
|
||||
>
|
||||
<TextInput
|
||||
id="website-url"
|
||||
type="text"
|
||||
inputMode="url"
|
||||
autoComplete="url"
|
||||
spellCheck={false}
|
||||
placeholder="https://example.com"
|
||||
value={url}
|
||||
invalid={Boolean(error)}
|
||||
disabled={disabled}
|
||||
onChange={(event) => onChange({ ...value, url: event.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{missingScheme ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onChange({ ...value, url: `https://${url.trim().replace(/^\/+/, "")}` })
|
||||
}
|
||||
className="mt-2 rounded-lg border border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-700 transition-colors hover:bg-slate-50"
|
||||
>
|
||||
https:// voranstellen
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user