feat: gemeinsames App-Design mit Seitenleiste, globalem Theme und neuem Login

This commit is contained in:
opencode committed 2026-10-18 11:11:30 +02:00
1 parent be6aac86ce
commit fa1e8c2c33
15 files changed
+1248 -189

No files matched your search

+85 -48
View File
@@ -1,21 +1,26 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Shell from "../../components/shell";
import { validateVoucher } from "@/lib/vouchers";
// Erweiterbar für Task 5 (Kontakte): zusätzliches Feld contactIds + <ContactSelect />
// ohne bestehende Felder anzufassen — Felder als Konfiguration belassen.
const FIELDS = [
{ name: "name", label: "Name", type: "text" },
{ name: "anbieter", label: "Anbieter", type: "text" },
{ name: "wertCents", label: "Wert (Cent)", type: "number" },
{ name: "restCents", label: "Rest (Cent, leer = Wert)", type: "number" },
{ name: "ablaufdatum", label: "Ablaufdatum", type: "date" },
{ name: "codeText", label: "Code", type: "text" },
{ name: "kategorie", label: "Kategorie", type: "text" },
{ name: "notiz", label: "Notiz", type: "text" },
{ name: "name", label: "Name", type: "text", placeholder: "z. B. Therme Erding" },
{ name: "anbieter", label: "Anbieter", type: "text", placeholder: "z. B. Therme Erding" },
{ name: "wertCents", label: "Wert (Cent)", type: "number", placeholder: "5000" },
{ name: "restCents", label: "Rest (Cent, leer = Wert)", type: "number", placeholder: "" },
{ name: "ablaufdatum", label: "Ablaufdatum", type: "date", placeholder: "" },
{ name: "codeText", label: "Code", type: "text", placeholder: "Code oder Nummer" },
{ name: "kategorie", label: "Kategorie", type: "text", placeholder: "z. B. Erlebnis" },
{ name: "notiz", label: "Notiz", type: "text", placeholder: "Optionale Notiz" },
] as const;
const inputClass =
"mt-1.5 h-11 w-full rounded-field border border-line bg-white px-3.5 text-sm text-ink placeholder:text-muted/60";
export default function NewVoucherPage() {
const router = useRouter();
const [values, setValues] = useState<Record<string, string>>({});
@@ -69,27 +74,37 @@ export default function NewVoucherPage() {
}
return (
<main className="mx-auto max-w-xl space-y-4 p-6">
<h1 className="text-xl font-bold">Gutschein anlegen</h1>
<form onSubmit={onSubmit} className="space-y-3" noValidate>
{FIELDS.map((f) => (
<div key={f.name}>
<label htmlFor={f.name} className="block text-sm font-medium">
{f.label}
</label>
<input
id={f.name}
name={f.name}
type={f.type}
value={values[f.name] ?? ""}
onChange={(e) => set(f.name, e.target.value)}
className="mt-1 w-full rounded border px-3 py-2"
/>
{errors.includes(f.name) && <p className="mt-1 text-sm text-red-600">Ungültig: {f.name}</p>}
</div>
))}
<div>
<label htmlFor="reminderDays" className="block text-sm font-medium">
<Shell>
<Link href="/" className="text-sm font-medium text-muted hover:text-ink">
← Zurück
</Link>
<h1 className="mt-2 text-2xl font-semibold tracking-tight">Gutschein anlegen</h1>
<p className="mt-1 text-sm text-muted">Alle Angaben lassen sich später noch ändern.</p>
<form onSubmit={onSubmit} className="mt-5 rounded-card border border-line bg-white p-5 sm:p-6" noValidate>
<div className="grid gap-4 sm:grid-cols-2">
{FIELDS.map((f) => (
<div key={f.name} className={f.name === "notiz" ? "sm:col-span-2" : undefined}>
<label htmlFor={f.name} className="block text-[13px] font-semibold">
{f.label}
</label>
<input
id={f.name}
name={f.name}
type={f.type}
value={values[f.name] ?? ""}
placeholder={f.placeholder || undefined}
onChange={(e) => set(f.name, e.target.value)}
aria-invalid={errors.includes(f.name)}
className={`${inputClass} ${errors.includes(f.name) ? "border-danger" : ""}`}
/>
{errors.includes(f.name) && (
<p className="mt-1 text-[13px] font-medium text-danger">Bitte prüfe dieses Feld.</p>
)}
</div>
))}
</div>
<div className="mt-4">
<label htmlFor="reminderDays" className="block text-[13px] font-semibold">
Erinnerung (Tage vorher, Kommaliste)
</label>
<input
@@ -99,30 +114,52 @@ export default function NewVoucherPage() {
value={reminderDays}
onChange={(e) => setReminderDays(e.target.value)}
placeholder="30,6,1"
className="mt-1 w-full rounded border px-3 py-2"
aria-invalid={errors.includes("reminderDays") || errors.includes("reminder")}
className={`${inputClass} ${errors.includes("reminderDays") || errors.includes("reminder") ? "border-danger" : ""}`}
/>
{errors.includes("reminderDays") && <p className="mt-1 text-sm text-red-600">Ungültig: reminderDays</p>}
{(errors.includes("reminderDays") || errors.includes("reminder")) && (
<p className="mt-1 text-[13px] font-medium text-danger">Mindestens ein gültiger Tag nötig.</p>
)}
</div>
<section id="kontakte">
<h2 className="text-sm font-medium">Empfänger</h2>
{contacts.map((c) => (
<label key={c.id} className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={contactIds.includes(c.id)}
onChange={(e) =>
setContactIds((ids) => (e.target.checked ? [...ids, c.id] : ids.filter((x) => x !== c.id)))
}
/>
{c.name}
</label>
))}
<section id="kontakte" aria-label="Empfänger" className="mt-5 rounded-field bg-wash p-4">
<h2 className="text-[13px] font-semibold">Empfänger</h2>
<p className="mt-0.5 text-[13px] text-muted">Wer soll an diesen Gutschein erinnert werden?</p>
{contacts.length === 0 ? (
<p className="mt-2 text-[13px] text-muted">
Noch keine Kontakte. Lege sie unter <Link href="/contacts" className="font-semibold text-brand-700 underline">Kontakte</Link> an.
</p>
) : (
<ul className="mt-2 grid gap-1.5">
{contacts.map((c) => (
<li key={c.id}>
<label className="flex cursor-pointer items-center gap-2.5 rounded-lg px-1 py-1 text-sm hover:bg-white">
<input
type="checkbox"
checked={contactIds.includes(c.id)}
onChange={(e) =>
setContactIds((ids) => (e.target.checked ? [...ids, c.id] : ids.filter((x) => x !== c.id)))
}
className="h-4 w-4 accent-brand-700"
/>
{c.name}
</label>
</li>
))}
</ul>
)}
</section>
{serverError && <p className="text-sm text-red-600">{serverError}</p>}
<button type="submit" className="rounded bg-black px-4 py-2 text-white">
{serverError && (
<p role="alert" className="mt-4 rounded-field bg-danger/10 px-3.5 py-2.5 text-sm font-medium text-danger">
{serverError}
</p>
)}
<button
type="submit"
className="mt-5 h-12 w-full rounded-field bg-brand-600 text-sm font-semibold text-white transition-colors hover:bg-brand-700 active:scale-[0.99] sm:w-auto sm:px-8"
>
Speichern
</button>
</form>
</main>
</Shell>
);
}