Apply linen design system across app

This commit is contained in:
t.indorf
2026-05-20 22:47:52 +02:00
parent 99ff978116
commit 1c5d2f10ac
56 changed files with 4698 additions and 2159 deletions
@@ -1,6 +1,6 @@
"use client";
import { useState, useTransition } from "react";
import { type FormEvent, useState, useTransition } from "react";
import { Pencil, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
@@ -16,6 +16,7 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getChildBirthdateRange } from "@/lib/child-birthdate";
import { createMyChild, deleteMyChild, updateMyChild } from "../actions";
export type MyChild = {
@@ -25,6 +26,18 @@ export type MyChild = {
dateOfBirth: string | null;
};
type ChildPayload = {
firstName: string;
lastName: string;
dateOfBirth: string;
};
const emptyChildPayload: ChildPayload = {
firstName: "",
lastName: "",
dateOfBirth: "",
};
export function MyChildrenManager({
items,
canManage,
@@ -33,38 +46,40 @@ export function MyChildrenManager({
canManage: boolean;
}) {
const [createOpen, setCreateOpen] = useState(false);
const [createPayload, setCreatePayload] =
useState<ChildPayload>(emptyChildPayload);
const [editingChild, setEditingChild] = useState<MyChild | null>(null);
const [editPayload, setEditPayload] =
useState<ChildPayload>(emptyChildPayload);
const [isPending, startTransition] = useTransition();
function payloadFromForm(formData: FormData) {
return {
firstName: String(formData.get("firstName") ?? ""),
lastName: String(formData.get("lastName") ?? ""),
dateOfBirth: String(formData.get("dateOfBirth") ?? ""),
};
function openEditor(child: MyChild) {
setEditingChild(child);
setEditPayload(payloadFromChild(child));
}
function handleCreate(formData: FormData) {
function handleCreate(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
startTransition(async () => {
const result = await createMyChild(payloadFromForm(formData));
const result = await createMyChild(createPayload);
if (result.error) {
toast.error(result.error);
return;
}
toast.success("Kind angelegt.");
setCreatePayload(emptyChildPayload);
setCreateOpen(false);
});
}
function handleUpdate(formData: FormData) {
function handleUpdate(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!editingChild) return;
startTransition(async () => {
const result = await updateMyChild(
editingChild.id,
payloadFromForm(formData),
);
const result = await updateMyChild(editingChild.id, editPayload);
if (result.error) {
toast.error(result.error);
return;
@@ -95,31 +110,45 @@ export function MyChildrenManager({
return (
<div className="space-y-3">
{items.length === 0 ? (
<p className="text-sm text-muted-foreground">Keine Kinder verknüpft.</p>
<p className="text-sm italic text-[var(--ink-faint)]">
Keine Kinder verknüpft.
</p>
) : (
<div className="flex flex-col gap-2">
{items.map((child) => (
<div
key={child.id}
className="flex items-center justify-between gap-3 rounded-md bg-muted/50 p-2"
className="flex items-center justify-between gap-3 rounded-lg bg-[var(--surface-2)] p-[10px_12px]"
>
<div>
<p className="font-medium">
{child.firstName} {child.lastName}
</p>
{child.dateOfBirth && (
<p className="text-xs text-muted-foreground">
Geboren am {new Date(child.dateOfBirth).toLocaleDateString("de-DE")}
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-[var(--accent-soft)] text-[11px] font-semibold text-[var(--accent-deep)]">
{getInitials(child)}
</div>
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-[var(--ink)]">
{child.firstName} {child.lastName}
</p>
)}
{child.dateOfBirth ? (
<p className="text-xs text-[var(--ink-soft)]">
Geboren am{" "}
{new Date(child.dateOfBirth).toLocaleDateString("de-DE")}
</p>
) : (
<p className="text-xs italic text-[var(--ink-faint)]">
Geburtsdatum fehlt
</p>
)}
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => setEditingChild(child)}
className="h-7 w-7"
onClick={() => openEditor(child)}
disabled={isPending || !canManage}
aria-label="Kind bearbeiten"
>
<Pencil className="h-4 w-4" />
</Button>
@@ -127,9 +156,10 @@ export function MyChildrenManager({
type="button"
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive"
className="h-7 w-7 text-[var(--danger)] hover:bg-[var(--danger-soft)] hover:text-[var(--danger)]"
onClick={() => handleDelete(child)}
disabled={isPending || !canManage}
aria-label="Kind löschen"
>
<Trash2 className="h-4 w-4" />
</Button>
@@ -140,22 +170,32 @@ export function MyChildrenManager({
)}
{canManage ? (
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<Dialog
open={createOpen}
onOpenChange={(open) => {
setCreateOpen(open);
if (!open) setCreatePayload(emptyChildPayload);
}}
>
<DialogTrigger asChild>
<Button type="button" variant="outline" size="sm" className="gap-2">
<Button type="button" variant="pill" size="sm" className="w-full">
<Plus className="h-4 w-4" />
Kind hinzufügen
</Button>
</DialogTrigger>
<DialogContent>
<form action={handleCreate}>
<form onSubmit={handleCreate}>
<DialogHeader>
<DialogTitle>Kind hinzufügen</DialogTitle>
<DialogDescription>
Dieses Kind wird mit deinem Haushalt verknüpft.
</DialogDescription>
</DialogHeader>
<ChildFields />
<ChildFields
idPrefix="create-child"
value={createPayload}
onChange={setCreatePayload}
/>
<DialogFooter>
<Button
type="button"
@@ -173,7 +213,7 @@ export function MyChildrenManager({
</DialogContent>
</Dialog>
) : (
<p className="text-xs text-muted-foreground">
<p className="text-xs italic text-[var(--ink-faint)]">
Dein Account ist noch keinem Haushalt zugeordnet.
</p>
)}
@@ -183,11 +223,17 @@ export function MyChildrenManager({
onOpenChange={(open) => !open && setEditingChild(null)}
>
<DialogContent>
<form action={handleUpdate}>
<form onSubmit={handleUpdate}>
<DialogHeader>
<DialogTitle>Kind bearbeiten</DialogTitle>
</DialogHeader>
{editingChild && <ChildFields child={editingChild} />}
{editingChild && (
<ChildFields
idPrefix="edit-child"
value={editPayload}
onChange={setEditPayload}
/>
)}
<DialogFooter>
<Button
type="button"
@@ -208,36 +254,84 @@ export function MyChildrenManager({
);
}
function ChildFields({ child }: { child?: MyChild }) {
function ChildFields({
idPrefix,
value,
onChange,
}: {
idPrefix: string;
value: ChildPayload;
onChange: (value: ChildPayload) => void;
}) {
const birthdateRange = getChildBirthdateRange();
return (
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="child-firstName">Vorname</Label>
<Label
htmlFor={`${idPrefix}-firstName`}
className="text-xs font-semibold text-[var(--ink-soft)]"
>
Vorname
</Label>
<Input
id="child-firstName"
id={`${idPrefix}-firstName`}
name="firstName"
defaultValue={child?.firstName ?? ""}
value={value.firstName}
onChange={(event) =>
onChange({ ...value, firstName: event.target.value })
}
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="child-lastName">Nachname</Label>
<Label
htmlFor={`${idPrefix}-lastName`}
className="text-xs font-semibold text-[var(--ink-soft)]"
>
Nachname
</Label>
<Input
id="child-lastName"
id={`${idPrefix}-lastName`}
name="lastName"
defaultValue={child?.lastName ?? ""}
value={value.lastName}
onChange={(event) =>
onChange({ ...value, lastName: event.target.value })
}
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="child-dateOfBirth">Geburtsdatum</Label>
<Label
htmlFor={`${idPrefix}-dateOfBirth`}
className="text-xs font-semibold text-[var(--ink-soft)]"
>
Geburtsdatum
</Label>
<Input
id="child-dateOfBirth"
id={`${idPrefix}-dateOfBirth`}
name="dateOfBirth"
type="date"
defaultValue={child?.dateOfBirth ?? ""}
min={birthdateRange.min}
max={birthdateRange.max}
value={value.dateOfBirth}
onChange={(event) =>
onChange({ ...value, dateOfBirth: event.target.value })
}
/>
</div>
</div>
);
}
function payloadFromChild(child: MyChild): ChildPayload {
return {
firstName: child.firstName,
lastName: child.lastName,
dateOfBirth: child.dateOfBirth ?? "",
};
}
function getInitials(child: MyChild) {
return `${child.firstName[0] ?? ""}${child.lastName[0] ?? ""}`.toUpperCase();
}