feat(admin): Listen mit Status-Badges, Leerzustaenden und klaren Aktionen
Neue gemeinsame Bausteine in components/admin-list.tsx: Seitenkopf mit Hauptaktion, Suchleiste, Status-Badge (gleiche Form wie der Bestellstatus, wartende Eintraege mit pulsierendem Punkt), Leerzustand mit Wabe und naechstem Schritt sowie Zeilenaktionen mit einheitlichem Unterstrich- Hover (Loeschen rot, Sperren/Zurueckziehen gedaempft). Umgestellt: Bewertungen (SVG-Sterne statt Unicode), Benutzer, Gutscheine (neu: Status "Abgelaufen"), Versand, Ernten, Chargen (Anlegeformulare auf admin-form), Firmenanfragen (Status als Badge, beschriftete Auswahl) und Audit-Log. Zahlen und Daten in Tabellen mit tabular-nums. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
95b4b5953d
commit
a09057d960
12 files changed
+440
-300
No files matched your search
@@ -0,0 +1,175 @@
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { LinkButton } from "@/components/ui";
|
||||
import { HexMark } from "@/components/honeycomb";
|
||||
|
||||
// Gemeinsame Bausteine der Admin-Listen (Kopfzeile, Suche, Status-Badge,
|
||||
// Leerzustand, Zeilenaktionen). Operate-Modus: gleiche Stelle, gleiche Form
|
||||
// auf jeder Liste, damit man beim Wechseln nicht neu suchen muss.
|
||||
|
||||
export function AdminPageHeader({
|
||||
title,
|
||||
intro,
|
||||
action,
|
||||
}: {
|
||||
title: string;
|
||||
intro?: ReactNode;
|
||||
/** Hauptaktion der Seite, z.B. "Neuer Gutschein". */
|
||||
action?: { href: string; label: string };
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 className="text-2xl text-forest">{title}</h1>
|
||||
{action ? (
|
||||
<LinkButton href={action.href} className="px-5 py-2.5">
|
||||
{action.label}
|
||||
</LinkButton>
|
||||
) : null}
|
||||
</div>
|
||||
{intro ? <p className="mt-2 max-w-2xl text-sm text-ink/70">{intro}</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminSearchForm({
|
||||
query,
|
||||
label,
|
||||
placeholder,
|
||||
submitLabel,
|
||||
resetHref,
|
||||
resetLabel,
|
||||
}: {
|
||||
query: string;
|
||||
label: string;
|
||||
placeholder: string;
|
||||
submitLabel: string;
|
||||
resetHref: string;
|
||||
resetLabel: string;
|
||||
}) {
|
||||
return (
|
||||
<form method="GET" role="search" className="mb-6 flex flex-wrap items-end gap-3">
|
||||
<label className="text-sm">
|
||||
<span className="mb-1 block text-xs text-ink/65">{label}</span>
|
||||
<input
|
||||
type="search"
|
||||
name="q"
|
||||
defaultValue={query}
|
||||
placeholder={placeholder}
|
||||
className="w-64 max-w-full border border-ink/20 bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
className="border border-forest px-4 py-2 text-sm text-forest transition-colors duration-300 hover:bg-forest hover:text-cream"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
{query ? (
|
||||
<Link href={resetHref} className="link-draw py-2 text-sm text-ink/65">
|
||||
{resetLabel}
|
||||
</Link>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export type AdminBadgeTone = "positive" | "pending" | "neutral" | "danger";
|
||||
|
||||
const BADGE_TONES: Record<AdminBadgeTone, string> = {
|
||||
positive: "border-forest/40 bg-forest/5 text-forest",
|
||||
pending: "border-honey/40 bg-honey/5 text-honey-dark",
|
||||
neutral: "border-ink/20 text-ink/70",
|
||||
danger: "border-red-200 bg-red-50 text-red-700",
|
||||
};
|
||||
|
||||
/** Status auf einen Blick — gleiche Form wie der Bestellstatus (OrderStatusBadge). */
|
||||
export function AdminBadge({
|
||||
tone,
|
||||
pulse = false,
|
||||
children,
|
||||
}: {
|
||||
tone: AdminBadgeTone;
|
||||
/** Wartet auf eine Handlung (z.B. Freigabe): Punkt pulsiert sanft. */
|
||||
pulse?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 whitespace-nowrap border px-2.5 py-1 text-xs tracking-wide-caps ${BADGE_TONES[tone]}`}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`h-1.5 w-1.5 rounded-full bg-current ${pulse ? "motion-pulse animate-[wh-pulse_2s_ease-out_infinite]" : ""}`}
|
||||
/>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Leerzustand statt einer blassen Textzeile: sagt, was fehlt, und bietet den naechsten Schritt an. */
|
||||
export function AdminEmptyState({
|
||||
message,
|
||||
action,
|
||||
bordered = false,
|
||||
}: {
|
||||
message: string;
|
||||
action?: { href: string; label: string };
|
||||
/** Eigener Rahmen, wenn der Leerzustand nicht in einer Tabellenkarte steht. */
|
||||
bordered?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col items-center gap-4 px-6 py-12 text-center ${bordered ? "border border-ink/10 bg-background" : ""}`}
|
||||
>
|
||||
<span className="relative inline-flex text-honey/60">
|
||||
<HexMark className="h-10 w-10" filled={false} />
|
||||
<HexMark className="absolute left-1/2 top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 text-honey" />
|
||||
</span>
|
||||
<p className="max-w-sm text-sm text-ink/70">{message}</p>
|
||||
{action ? (
|
||||
<LinkButton href={action.href} variant="secondary" className="px-5 py-2.5">
|
||||
{action.label}
|
||||
</LinkButton>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminRowActions({ children }: { children: ReactNode }) {
|
||||
return <div className="flex items-center justify-end gap-4 whitespace-nowrap">{children}</div>;
|
||||
}
|
||||
|
||||
export function AdminActionLink({ href, children }: { href: string; children: ReactNode }) {
|
||||
return (
|
||||
<Link href={href} className="link-draw text-forest">
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
const ACTION_TONES = {
|
||||
default: "text-forest",
|
||||
muted: "text-ink/70",
|
||||
danger: "text-red-700",
|
||||
} as const;
|
||||
|
||||
/** Absende-Knopf einer Zeilenaktion — sieht aus wie ein Link, ist aber ein Formular-Submit. */
|
||||
export function AdminActionButton({
|
||||
tone = "default",
|
||||
children,
|
||||
}: {
|
||||
tone?: keyof typeof ACTION_TONES;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button type="submit" className={`link-draw cursor-pointer ${ACTION_TONES[tone]}`}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/** Grund, warum eine Aktion fehlt — statt den Knopf wortlos wegzulassen. */
|
||||
export function AdminActionNote({ children }: { children: ReactNode }) {
|
||||
return <span className="text-xs text-ink/55">{children}</span>;
|
||||
}
|
||||
Reference in new issue
Block a user