Files
Wabenhain/src/app/admin/benutzer/[id]/page.tsx
T
Nick SzaunigandClaude Opus 5.5 6950657c91 feat(ui): Dunkelmodus fuer Shop und Admin mit Umschalter
Rollen-Farben statt reiner Markenfarben, damit hell und dunkel aus einem
Satz Klassen entstehen:
- heading (Ueberschriften, Links, Rahmen): hell Waldgruen, dunkel Creme
- surface (abgesetzte Flaeche): hell Creme, dunkel angehobenes Dunkelgruen
- forest bleibt die dunkle Markenflaeche (Hero, Baender, Knoepfe,
  Admin-Leiste), cream der helle Text darauf
- honey-deep als fester Honigton fuer Flaechen; honey-dark (Text) wird
  im Dunkelmodus heller und bleibt lesbar
Codemod ueber src: text-/border-forest -> *-heading, bg-cream ->
bg-surface, bg-honey-dark -> bg-honey-deep. Im hellen Modus haben die
Rollen exakt die bisherigen Werte - das Erscheinungsbild bleibt gleich.

Umschalter (components/theme-toggle.tsx) Automatisch -> Hell -> Dunkel in
Kopfzeile, Mobilmenue und Admin-Leiste. Die Wahl liegt im Cookie "theme"
(nur light/dark, geprueft in lib/theme.ts); der Server setzt data-theme
schon beim Ausliefern, nichts blitzt hell auf. "Automatisch" folgt der
Systemeinstellung. Gewaehlter und automatischer Dunkelblock in
globals.css werden per Test auf Gleichheit geprueft.

Logo, Biene und Select-Pfeil nutzen die Rollenfarben. In der Kopfzeile
steht "Honig entdecken" zwischen 1280 und 1535 px nicht mehr doppelt
neben "Shop" (sonst stiess die Navigation an), der Slogan erscheint erst
ab 1680 px.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 06:12:44 +02:00

128 lines
5.5 KiB
TypeScript

import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { ConfirmForm } from "@/components/confirm-form";
import { Button } from "@/components/ui";
import { getCurrentAdmin } from "@/lib/auth";
import { formatDate } from "@/lib/format";
import { AdminField, AdminFieldset, AdminFormPanel, AdminSelect } from "@/components/admin-form";
import { AdminActionButton, AdminBadge } from "@/components/admin-list";
import {
updateCustomerAction,
blockCustomerAction,
unblockCustomerAction,
deleteCustomerAction,
sendCustomerPasswordResetAction,
} from "../../actions";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminBenutzerEditPage(props: PageProps<"/admin/benutzer/[id]">) {
const t = getDictionary(await getLocale());
const tu = t.adminUserEditPage;
const { id } = await props.params;
const [admin, customer] = await Promise.all([
getCurrentAdmin(),
prisma.customer.findUnique({ where: { id } }),
]);
if (!customer) notFound();
const isSelf = customer.id === admin?.id;
const isTemporarilyLocked = Boolean(customer.lockedUntil && customer.lockedUntil > new Date());
return (
<div className="max-w-2xl">
<BackLink href="/admin/benutzer" label={tu.backToUsers} />
<h1 className="text-2xl text-heading mb-2">
{customer.firstName} {customer.lastName}
</h1>
<p className="text-sm text-ink/65 mb-8">{tu.createdOn(formatDate(customer.createdAt))}</p>
<AdminFormPanel action={updateCustomerAction} actions={<Button type="submit">{tu.save}</Button>}>
<input type="hidden" name="id" value={customer.id} />
<AdminFieldset legend={tu.sectionProfile} last>
<div className="grid grid-cols-2 items-end gap-4">
<AdminField name="firstName" label={tu.firstNameLabel} defaultValue={customer.firstName} required />
<AdminField name="lastName" label={tu.lastNameLabel} defaultValue={customer.lastName} required />
</div>
<AdminField name="email" label={tu.emailLabel} type="email" defaultValue={customer.email} required />
<AdminField name="phone" label={tu.phoneLabel} type="tel" defaultValue={customer.phone ?? ""} />
<AdminSelect
name="role"
label={tu.roleLabel}
defaultValue={customer.role}
hint={isSelf ? tu.cannotChangeOwnRole : undefined}
>
<option value="KUNDE">{tu.roleCustomer}</option>
<option value="ADMIN">{tu.roleAdmin}</option>
</AdminSelect>
</AdminFieldset>
</AdminFormPanel>
<section className="mt-8 space-y-6 border border-ink/10 bg-background p-6 sm:p-8">
<h2 className="font-serif text-xl text-heading">{tu.securityHeading}</h2>
<dl className="divide-y divide-ink/10 text-sm">
<div className="flex items-center justify-between gap-4 py-3">
<dt className="text-ink/70">{tu.twoFactorLabel}</dt>
<dd>
{customer.twoFactorEnabled ? (
<AdminBadge tone="positive">{tu.twoFactorActive}</AdminBadge>
) : (
<AdminBadge tone="neutral">{tu.twoFactorInactive}</AdminBadge>
)}
</dd>
</div>
<div className="flex items-center justify-between gap-4 py-3">
<dt className="text-ink/70">{tu.statusLabel}</dt>
<dd className="text-right">
{customer.blockedAt ? (
<AdminBadge tone="danger">{tu.blockedSince(formatDate(customer.blockedAt))}</AdminBadge>
) : isTemporarilyLocked ? (
<AdminBadge tone="pending">{tu.temporarilyLockedFailedAttempts}</AdminBadge>
) : (
<AdminBadge tone="positive">{tu.statusActive}</AdminBadge>
)}
</dd>
</div>
</dl>
<form action={sendCustomerPasswordResetAction}>
<input type="hidden" name="id" value={customer.id} />
<Button type="submit" variant="secondary" className="px-5 py-2.5">
{tu.sendPasswordReset}
</Button>
</form>
{/* Sperren/Loeschen des eigenen Kontos ist serverseitig ohnehin
verboten — hier gar nicht erst angeboten. */}
{!isSelf ? (
<div className="border-t border-ink/10 pt-6">
<h3 className="mb-3 text-xs tracking-wide-caps text-ink/65">{tu.sectionDanger}</h3>
<div className="flex flex-wrap items-center gap-6 text-sm">
{customer.blockedAt ? (
<form action={unblockCustomerAction}>
<input type="hidden" name="id" value={customer.id} />
<AdminActionButton>{tu.unblock}</AdminActionButton>
</form>
) : (
<form action={blockCustomerAction}>
<input type="hidden" name="id" value={customer.id} />
<AdminActionButton tone="muted">{tu.block}</AdminActionButton>
</form>
)}
<ConfirmForm
action={deleteCustomerAction}
confirmMessage={tu.deleteConfirm(`${customer.firstName} ${customer.lastName}`)}
>
<input type="hidden" name="id" value={customer.id} />
<AdminActionButton tone="danger">{tu.deleteAccount}</AdminActionButton>
</ConfirmForm>
</div>
</div>
) : null}
</section>
</div>
);
}