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]>
275 lines
9.5 KiB
TypeScript
275 lines
9.5 KiB
TypeScript
import Link from "next/link";
|
|
import type { ReactNode } from "react";
|
|
|
|
/**
|
|
* Verlinkte Karte fuer die Konto-Uebersicht (Bestellungen, Adressen, ...).
|
|
* Ersetzt den zuvor identischen, icon-losen "border + hover:border-heading"-
|
|
* Kartenstil, der auf jeder Konto-Seite gleich aussah.
|
|
*/
|
|
export function AccountNavCard({
|
|
href,
|
|
icon,
|
|
title,
|
|
description,
|
|
accent = false,
|
|
className = "",
|
|
}: {
|
|
href: string;
|
|
icon: ReactNode;
|
|
title: string;
|
|
description: string;
|
|
accent?: boolean;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<Link
|
|
href={href}
|
|
className={`group relative flex items-start gap-5 border p-6 transition-[transform,border-color,box-shadow] duration-500 ease-[var(--ease-arrive)] hover:-translate-y-1 ${
|
|
accent
|
|
? "border-honey/40 bg-honey/5 hover:border-honey hover:shadow-[0_22px_40px_-24px_rgba(184,138,68,0.45)]"
|
|
: "border-ink/10 bg-background hover:border-heading/60 hover:shadow-[0_22px_40px_-24px_rgba(36,53,43,0.35)]"
|
|
} ${className}`}
|
|
>
|
|
{/* Icon in einer Wabe, die sich beim Hover mit Honig fuellt. */}
|
|
<span className="relative flex-shrink-0 flex items-center justify-center w-12 h-[3.4rem]">
|
|
<span aria-hidden="true" className={`clip-hex absolute inset-0 ${accent ? "bg-honey/30" : "bg-ink/10"}`} />
|
|
<span
|
|
aria-hidden="true"
|
|
className="clip-hex absolute inset-[1.5px] bg-background transition-colors duration-500 group-hover:bg-honey"
|
|
/>
|
|
<span
|
|
className={`relative transition-colors duration-500 group-hover:text-cream ${
|
|
accent ? "text-honey-dark" : "text-heading"
|
|
}`}
|
|
>
|
|
{icon}
|
|
</span>
|
|
</span>
|
|
<span className="flex-1 pt-1">
|
|
<span className="block font-serif text-2xl leading-tight text-heading">{title}</span>
|
|
<span className="block mt-1.5 text-sm text-ink/70">{description}</span>
|
|
</span>
|
|
<svg
|
|
aria-hidden="true"
|
|
viewBox="0 0 24 24"
|
|
className="mt-2 h-4 w-4 flex-shrink-0 -translate-x-2 text-heading opacity-0 transition-[opacity,transform] duration-500 ease-[var(--ease-arrive)] group-hover:translate-x-0 group-hover:opacity-100"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.6"
|
|
>
|
|
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
|
|
</svg>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
const ORDER_STATUS_STYLES: Record<string, string> = {
|
|
NEU: "border-ink/20 text-ink/70",
|
|
BEZAHLT: "border-honey/40 text-honey-dark bg-honey/5",
|
|
IN_VORBEREITUNG: "border-honey/40 text-honey-dark bg-honey/5",
|
|
VERSANDBEREIT: "border-honey/40 text-honey-dark bg-honey/5",
|
|
VERSENDET: "border-heading/40 text-heading bg-forest/5",
|
|
ABHOLBEREIT: "border-heading/40 text-heading bg-forest/5",
|
|
ABGEHOLT: "border-heading/40 text-heading bg-forest/5",
|
|
STORNIERT: "border-red-200 text-red-700 bg-red-50",
|
|
};
|
|
|
|
/** Farbcodiertes Status-Label statt reinem Fließtext, damit der Bestellstatus auf einen Blick erkennbar ist. */
|
|
/** Status, bei denen gerade etwas passiert — ihr Punkt pulsiert sanft. */
|
|
const ORDER_STATUS_IN_PROGRESS = new Set(["BEZAHLT", "IN_VORBEREITUNG", "VERSANDBEREIT", "ABHOLBEREIT"]);
|
|
|
|
export function OrderStatusBadge({ status, label }: { status: string; label: string }) {
|
|
const style = ORDER_STATUS_STYLES[status] ?? "border-ink/20 text-ink/70";
|
|
return (
|
|
<span className={`inline-flex items-center gap-2 border px-2.5 py-1 text-xs tracking-wide-caps ${style}`}>
|
|
<span
|
|
aria-hidden="true"
|
|
className={`h-1.5 w-1.5 rounded-full bg-current ${
|
|
ORDER_STATUS_IN_PROGRESS.has(status) ? "motion-pulse animate-[wh-pulse_2s_ease-out_infinite]" : ""
|
|
}`}
|
|
/>
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const VERSAND_STEP_KEYS = ["NEU", "BEZAHLT", "IN_VORBEREITUNG", "VERSANDBEREIT", "VERSENDET"] as const;
|
|
const ABHOLUNG_STEP_KEYS = ["NEU", "BEZAHLT", "IN_VORBEREITUNG", "ABHOLBEREIT", "ABGEHOLT"] as const;
|
|
|
|
export interface OrderStatusTimelineLabels {
|
|
ordered: string;
|
|
paid: string;
|
|
preparing: string;
|
|
readyToShip: string;
|
|
shipped: string;
|
|
readyForPickup: string;
|
|
pickedUp: string;
|
|
cancelled: string;
|
|
ariaLabel: string;
|
|
}
|
|
|
|
/**
|
|
* Visuelle Fortschrittsanzeige statt reinem Status-Badge — nimmt das
|
|
* Punkt/Linie-Muster der Chargen-Seite (app/charge/[code]) auf, damit
|
|
* Kundschaft auf einen Blick sieht, wie weit die Bestellung bereits ist,
|
|
* statt nur den aktuellen Einzelstatus als Wort zu lesen. Nimmt uebersetzte
|
|
* Labels als Prop entgegen (statt selbst zu uebersetzen), damit diese Datei
|
|
* server-sicher bleibt — sie wird auch von Admin-Server-Seiten importiert.
|
|
*/
|
|
export function OrderStatusTimeline({
|
|
status,
|
|
fulfillmentMethod,
|
|
labels,
|
|
}: {
|
|
status: string;
|
|
fulfillmentMethod: "VERSAND" | "ABHOLUNG";
|
|
labels: OrderStatusTimelineLabels;
|
|
}) {
|
|
if (status === "STORNIERT") {
|
|
return (
|
|
<div className="flex items-center gap-2 text-sm text-red-700">
|
|
<span className="w-2 h-2 rounded-full bg-red-500" aria-hidden="true" />
|
|
{labels.cancelled}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const stepLabels: Record<(typeof VERSAND_STEP_KEYS)[number] | (typeof ABHOLUNG_STEP_KEYS)[number], string> = {
|
|
NEU: labels.ordered,
|
|
BEZAHLT: labels.paid,
|
|
IN_VORBEREITUNG: labels.preparing,
|
|
VERSANDBEREIT: labels.readyToShip,
|
|
VERSENDET: labels.shipped,
|
|
ABHOLBEREIT: labels.readyForPickup,
|
|
ABGEHOLT: labels.pickedUp,
|
|
};
|
|
const stepKeys = fulfillmentMethod === "VERSAND" ? VERSAND_STEP_KEYS : ABHOLUNG_STEP_KEYS;
|
|
const steps = stepKeys.map((key) => ({ key, label: stepLabels[key] }));
|
|
const currentIndex = steps.findIndex((step) => step.key === status);
|
|
|
|
return (
|
|
<div className="flex items-start gap-x-1" role="list" aria-label={labels.ariaLabel}>
|
|
{steps.map((step, i) => {
|
|
const isDone = currentIndex >= 0 && i <= currentIndex;
|
|
const isLineDone = i < currentIndex;
|
|
return (
|
|
<div key={step.key} role="listitem" className="flex-1 min-w-0 relative">
|
|
<div className="flex items-center">
|
|
<svg
|
|
viewBox="0 0 24 26"
|
|
aria-hidden="true"
|
|
className={`h-3.5 w-3.5 flex-shrink-0 rounded-full ${
|
|
i === currentIndex && i < steps.length - 1 ? "motion-pulse animate-[wh-pulse_2.2s_ease-out_infinite]" : ""
|
|
}`}
|
|
>
|
|
<polygon
|
|
points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7"
|
|
fill={isDone ? "var(--color-honey)" : "var(--color-background)"}
|
|
stroke={isDone ? "var(--color-honey-dark)" : "color-mix(in oklab, var(--color-ink) 25%, transparent)"}
|
|
strokeWidth="2"
|
|
/>
|
|
</svg>
|
|
{i < steps.length - 1 ? (
|
|
<span className="relative ml-1 h-[2px] flex-1 overflow-hidden rounded-full bg-ink/10" aria-hidden="true">
|
|
{isLineDone ? (
|
|
// Erledigte Abschnitte fliessen nacheinander voll — der
|
|
// Fortschritt wird beim Oeffnen der Seite nachvollzogen.
|
|
<span
|
|
className="motion-grow absolute inset-0 origin-left bg-honey animate-[wh-grow_0.55s_var(--ease-arrive)_both]"
|
|
style={{ animationDelay: `${150 + i * 140}ms` }}
|
|
/>
|
|
) : null}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<p className={`mt-2 text-[11px] text-center leading-tight ${isDone ? "text-heading" : "text-ink/55"}`}>
|
|
{step.label}
|
|
</p>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Leerer-Zustand mit Icon statt eines einzelnen, unauffälligen Fließtext-Satzes. */
|
|
export function AccountEmptyState({
|
|
icon,
|
|
title,
|
|
description,
|
|
action,
|
|
}: {
|
|
icon: ReactNode;
|
|
title: string;
|
|
description?: string;
|
|
action?: ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="mt-10 border border-dashed border-ink/15 px-8 py-16 text-center">
|
|
<span className="relative mx-auto flex h-16 w-14 items-center justify-center text-honey-dark">
|
|
<svg viewBox="0 0 24 26" aria-hidden="true" className="absolute inset-0 h-full w-full text-honey">
|
|
<polygon
|
|
points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="0.8"
|
|
strokeDasharray="2 1.6"
|
|
/>
|
|
</svg>
|
|
<span className="relative">{icon}</span>
|
|
</span>
|
|
<p className="mt-5 font-serif text-2xl text-heading">{title}</p>
|
|
{description ? (
|
|
<p className="mt-1 text-sm text-ink/70 max-w-sm mx-auto">{description}</p>
|
|
) : null}
|
|
{action ? <div className="mt-6">{action}</div> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const ICON_PROPS = {
|
|
width: 20,
|
|
height: 20,
|
|
viewBox: "0 0 24 24",
|
|
fill: "none",
|
|
stroke: "currentColor",
|
|
strokeWidth: 1.5,
|
|
strokeLinecap: "round" as const,
|
|
strokeLinejoin: "round" as const,
|
|
};
|
|
|
|
export function OrderIcon() {
|
|
return (
|
|
<svg {...ICON_PROPS}>
|
|
<path d="M4 8l8-4 8 4-8 4-8-4z" />
|
|
<path d="M4 8v8l8 4 8-4V8" />
|
|
<path d="M12 12v8" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function AddressIcon() {
|
|
return (
|
|
<svg {...ICON_PROPS}>
|
|
<path d="M12 21s7-7.5 7-12a7 7 0 1 0-14 0c0 4.5 7 12 7 12z" />
|
|
<circle cx="12" cy="9" r="2.5" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function HeartIcon() {
|
|
return (
|
|
<svg {...ICON_PROPS}>
|
|
<path d="M12 20s-7-4.5-9.5-9A5 5 0 0 1 12 6a5 5 0 0 1 9.5 5c-2.5 4.5-9.5 9-9.5 9z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function ShieldIcon() {
|
|
return (
|
|
<svg {...ICON_PROPS}>
|
|
<path d="M12 3l7 3v6c0 4.5-3 7.5-7 9-4-1.5-7-4.5-7-9V6l7-3z" />
|
|
</svg>
|
|
);
|
|
}
|