282 lines
9.4 KiB
TypeScript
282 lines
9.4 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState, useState } from "react";
|
|
import { Plus, Trash2 } from "lucide-react";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { addFamilyAction, type AddFamilyState } from "./actions";
|
|
|
|
const initialState: AddFamilyState = {};
|
|
|
|
// =====================================================================
|
|
// AddFamilyDialog · Client Component
|
|
// ---------------------------------------------------------------------
|
|
// Verwaltet:
|
|
// • Dialog-Open/Close-State
|
|
// • Dynamische Kinderliste (min. 1, max. 10)
|
|
// • Ein oder zwei Elternteile pro Haushalt
|
|
// • useActionState → Server-Action-Fehler anzeigen
|
|
// • Bei Erfolg (state.success) Dialog automatisch schließen
|
|
// =====================================================================
|
|
|
|
export function AddFamilyDialog() {
|
|
const [open, setOpen] = useState(false);
|
|
const [childCount, setChildCount] = useState(1);
|
|
const [state, formAction, pending] = useActionState(
|
|
addFamilyAction,
|
|
initialState,
|
|
);
|
|
|
|
// Dialog schließen, wenn die Action erfolgreich war
|
|
if (state.success && open) {
|
|
setOpen(false);
|
|
}
|
|
|
|
function handleOpenChange(nextOpen: boolean) {
|
|
setOpen(nextOpen);
|
|
// State beim Schließen zurücksetzen würde useActionState erfordern —
|
|
// stattdessen schließen wir einfach und der State bleibt bis zur nächsten
|
|
// Aktion erhalten (unkritisch).
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm">
|
|
<Plus className="h-4 w-4" />
|
|
Familie hinzufügen
|
|
</Button>
|
|
</DialogTrigger>
|
|
|
|
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
|
|
<DialogHeader>
|
|
<DialogTitle>Familie hinzufügen</DialogTitle>
|
|
<DialogDescription>
|
|
Lege einen Haushalt mit Elternteilen und Kindern an. Die
|
|
Elternteile erhalten Einladungslinks per E-Mail.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<form action={formAction} className="space-y-6">
|
|
{/* Anzahl Kinder als verstecktes Feld für die Server Action */}
|
|
<input type="hidden" name="childCount" value={childCount} />
|
|
|
|
<FormField
|
|
id="familyName"
|
|
name="familyName"
|
|
label="Familienname"
|
|
placeholder="Familie Schmidt"
|
|
error={state.errors?.familyName?.[0]}
|
|
/>
|
|
|
|
{/* ── Elternteile ────────────────────────────────────────── */}
|
|
<fieldset className="space-y-4">
|
|
<legend className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
|
|
Elternteil 1
|
|
</legend>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<FormField
|
|
id="parent1FirstName"
|
|
name="parent1FirstName"
|
|
label="Vorname"
|
|
autoComplete="given-name"
|
|
error={state.errors?.parent1FirstName?.[0]}
|
|
/>
|
|
<FormField
|
|
id="parent1LastName"
|
|
name="parent1LastName"
|
|
label="Nachname"
|
|
autoComplete="family-name"
|
|
error={state.errors?.parent1LastName?.[0]}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
id="parent1Email"
|
|
name="parent1Email"
|
|
type="email"
|
|
label="E-Mail-Adresse"
|
|
autoComplete="email"
|
|
error={state.errors?.parent1Email?.[0]}
|
|
/>
|
|
</fieldset>
|
|
|
|
<fieldset className="space-y-4">
|
|
<legend>
|
|
<span className="text-[11px] uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
|
|
Elternteil 2
|
|
</span>{" "}
|
|
<span className="font-normal italic text-[var(--ink-faint)]">
|
|
optional
|
|
</span>
|
|
</legend>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<FormField
|
|
id="parent2FirstName"
|
|
name="parent2FirstName"
|
|
label="Vorname"
|
|
autoComplete="given-name"
|
|
required={false}
|
|
error={state.errors?.parent2FirstName?.[0]}
|
|
/>
|
|
<FormField
|
|
id="parent2LastName"
|
|
name="parent2LastName"
|
|
label="Nachname"
|
|
autoComplete="family-name"
|
|
required={false}
|
|
error={state.errors?.parent2LastName?.[0]}
|
|
/>
|
|
</div>
|
|
|
|
<FormField
|
|
id="parent2Email"
|
|
name="parent2Email"
|
|
type="email"
|
|
label="E-Mail-Adresse"
|
|
autoComplete="email"
|
|
required={false}
|
|
error={state.errors?.parent2Email?.[0]}
|
|
/>
|
|
</fieldset>
|
|
|
|
{/* ── Kinder ─────────────────────────────────────────────── */}
|
|
<fieldset className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<legend className="text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
|
|
Kinder ({childCount})
|
|
</legend>
|
|
{childCount < 10 && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setChildCount((c) => c + 1)}
|
|
>
|
|
<Plus className="mr-1.5 h-3.5 w-3.5" />
|
|
Kind hinzufügen
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{Array.from({ length: childCount }).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className="relative rounded-lg border border-[var(--hairline)] bg-[var(--surface-2)] p-4"
|
|
>
|
|
{childCount > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
// Kind entfernen — childCount reduzieren reicht, da die
|
|
// Felder nach Index benannt sind und der letzte entfernt wird.
|
|
setChildCount((c) => c - 1);
|
|
}}
|
|
className="absolute right-3 top-3 rounded-md p-1 text-[var(--ink-muted)] transition-colors hover:bg-[var(--danger-soft)] hover:text-[var(--danger)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--surface-2)]"
|
|
aria-label={`Kind ${i + 1} entfernen`}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
)}
|
|
<p className="mb-3 text-[11px] font-semibold uppercase text-[var(--ink-muted)] [letter-spacing:0.12em]">
|
|
Kind {i + 1}
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<FormField
|
|
id={`childFirstName_${i}`}
|
|
name={`childFirstName_${i}`}
|
|
label="Vorname"
|
|
/>
|
|
<FormField
|
|
id={`childLastName_${i}`}
|
|
name={`childLastName_${i}`}
|
|
label="Nachname"
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{state.errors?.children?.[0] && (
|
|
<p className="text-xs text-[var(--danger)]">
|
|
{state.errors.children[0]}
|
|
</p>
|
|
)}
|
|
</fieldset>
|
|
|
|
{/* Globaler Fehler */}
|
|
{state.errors?._form?.[0] && (
|
|
<p className="rounded-lg border border-[var(--danger)] bg-[var(--danger-soft)] p-3 text-sm text-[var(--danger)]">
|
|
{state.errors._form[0]}
|
|
</p>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
onClick={() => setOpen(false)}
|
|
disabled={pending}
|
|
>
|
|
Abbrechen
|
|
</Button>
|
|
<Button type="submit" disabled={pending}>
|
|
{pending ? "Wird angelegt…" : "Familie anlegen"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
// ── Hilfskomponente ────────────────────────────────────────────────────
|
|
|
|
function FormField({
|
|
id,
|
|
name,
|
|
label,
|
|
type = "text",
|
|
autoComplete,
|
|
placeholder,
|
|
required = true,
|
|
error,
|
|
}: {
|
|
id: string;
|
|
name: string;
|
|
label: string;
|
|
type?: string;
|
|
autoComplete?: string;
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
error?: string;
|
|
}) {
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={id}>{label}</Label>
|
|
<Input
|
|
id={id}
|
|
name={name}
|
|
type={type}
|
|
autoComplete={autoComplete}
|
|
placeholder={placeholder}
|
|
required={required}
|
|
aria-invalid={!!error}
|
|
/>
|
|
{error && <p className="text-xs text-[var(--danger)]">{error}</p>}
|
|
</div>
|
|
);
|
|
}
|