Docker-Stack, README und Korrekturen aus dem ersten Durchlauf
- Dockerfile (4 Stufen) und docker-compose.yml mit Postgres-Healthcheck, Volume und direktem Port-Mapping 3000:3000, ohne Reverse Proxy. - Entrypoint wendet beim Start "prisma migrate deploy" an; die Prisma-CLI bekommt eine eigene Build-Stufe, weil sie mehr als nur @prisma/* braucht. - prisma.config.ts durch prisma.config.js ersetzt: laedt ohne TypeScript und ohne dotenv, damit sie im Laufzeit-Image funktioniert. - Prisma-Client wird erst beim ersten Zugriff erzeugt. Vorher scheiterte "next build" beim Einsammeln der Routen-Konfiguration ohne DATABASE_URL. - Postgres-18-Volume auf /var/lib/postgresql gemountet (neues Datenlayout). - npm 11 blockiert Installationsskripte: erlaubte Pakete stehen jetzt im Feld "allowScripts" der package.json. - Lokale Variable "module" ueberall in "qrType" umbenannt (verdeckte sonst die CommonJS-Variable) und Vorschau-Effekt ohne synchrones setState. - README.md auf Deutsch: Setup, Konfiguration, Bedienung, Aufbau, API und Ausblick auf Version 2.
This commit is contained in:
@@ -43,10 +43,10 @@ function sameData(a: unknown, b: unknown): boolean {
|
||||
}
|
||||
|
||||
export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
const module = qrTypes[typeId];
|
||||
const qrType = qrTypes[typeId];
|
||||
const Form = qrForms[typeId];
|
||||
|
||||
const [data, setData] = useState<QrFormData>(module.defaultData);
|
||||
const [data, setData] = useState<QrFormData>(qrType.defaultData);
|
||||
const [design, setDesign] = useState<QrDesign>(defaultDesign);
|
||||
const [isDynamic, setIsDynamic] = useState(false);
|
||||
const [label, setLabel] = useState("");
|
||||
@@ -62,14 +62,14 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
const [downloading, setDownloading] = useState<"png" | "svg" | null>(null);
|
||||
const [downloadError, setDownloadError] = useState<string | null>(null);
|
||||
|
||||
const validation = useMemo(() => module.validate(data), [module, data]);
|
||||
const validation = useMemo(() => qrType.validate(data), [qrType, data]);
|
||||
const designValid = useMemo(
|
||||
() => designSchema.safeParse(design).success,
|
||||
[design],
|
||||
);
|
||||
|
||||
// Fehler erst zeigen, wenn tatsächlich etwas eingegeben wurde.
|
||||
const touched = !sameData(data, module.defaultData);
|
||||
const touched = !sameData(data, qrType.defaultData);
|
||||
const dataError = !validation.ok && touched ? validation.message : null;
|
||||
|
||||
const mode = isDynamic ? "dynamic" : "static";
|
||||
@@ -87,18 +87,13 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
const requestId = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ready) {
|
||||
setPreview(null);
|
||||
setPreviewError(null);
|
||||
setPreviewLoading(false);
|
||||
return;
|
||||
}
|
||||
if (!ready) return;
|
||||
|
||||
const id = (requestId.current += 1);
|
||||
const controller = new AbortController();
|
||||
setPreviewLoading(true);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setPreviewLoading(true);
|
||||
apiJson<PreviewState>("/api/qr/preview", {
|
||||
method: "POST",
|
||||
json: requestBody,
|
||||
@@ -129,6 +124,13 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
};
|
||||
}, [ready, requestBody]);
|
||||
|
||||
// Solange die Eingaben unvollständig sind, gibt es nichts anzuzeigen. Das
|
||||
// wird abgeleitet statt im Effekt zurückgesetzt — so bleibt beim Tippen die
|
||||
// zuletzt gültige Vorschau stehen, statt bei jedem Zeichen zu flackern.
|
||||
const shownPreview = ready ? preview : null;
|
||||
const shownPreviewError = ready ? previewError : null;
|
||||
const shownPreviewLoading = ready && previewLoading;
|
||||
|
||||
/* ------------------------------------------------------------ Download */
|
||||
|
||||
const downloadDisabledReason =
|
||||
@@ -224,13 +226,13 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
|
||||
<div className="mb-7 flex flex-wrap items-center gap-3">
|
||||
<span className="flex size-11 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600">
|
||||
<Icon name={module.icon} className="size-6" />
|
||||
<Icon name={qrType.icon} className="size-6" />
|
||||
</span>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900">
|
||||
{module.title}-QR-Code erstellen
|
||||
{qrType.title}-QR-Code erstellen
|
||||
</h1>
|
||||
<p className="text-sm text-slate-600">{module.description}</p>
|
||||
<p className="text-sm text-slate-600">{qrType.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -279,7 +281,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isDynamic}
|
||||
disabled={!module.supportsDynamic}
|
||||
disabled={!qrType.supportsDynamic}
|
||||
onClick={() => setIsDynamic(true)}
|
||||
className={cx(
|
||||
"rounded-xl border p-4 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-50",
|
||||
@@ -353,7 +355,7 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
href="/meine-codes"
|
||||
className="inline-flex items-center gap-1.5 rounded-lg text-sm font-medium text-indigo-600 hover:text-indigo-700"
|
||||
>
|
||||
Zu „Meine Codes"
|
||||
Zu „Meine Codes“
|
||||
<Icon name="external" className="size-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
@@ -401,11 +403,11 @@ export function QrGenerator({ typeId }: { typeId: QrTypeId }) {
|
||||
<Card>
|
||||
<CardTitle>Vorschau</CardTitle>
|
||||
<QrPreview
|
||||
dataUrl={preview?.dataUrl ?? null}
|
||||
content={preview?.content ?? null}
|
||||
dataUrl={shownPreview?.dataUrl ?? null}
|
||||
content={shownPreview?.content ?? null}
|
||||
size={design.size}
|
||||
error={previewError ?? downloadError}
|
||||
loading={previewLoading}
|
||||
error={shownPreviewError ?? downloadError}
|
||||
loading={shownPreviewLoading}
|
||||
downloadDisabledReason={downloadDisabledReason}
|
||||
onDownload={handleDownload}
|
||||
downloading={downloading}
|
||||
|
||||
Reference in New Issue
Block a user