feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -0,0 +1,222 @@
|
||||
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-forest"-
|
||||
* 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 flex items-start gap-4 border p-6 transition-all duration-300 hover:-translate-y-0.5 ${
|
||||
accent
|
||||
? "border-honey/40 bg-honey/5 hover:border-honey hover:shadow-[0_16px_32px_-20px_rgba(184,138,68,0.35)]"
|
||||
: "border-ink/10 hover:border-forest hover:shadow-[0_16px_32px_-20px_rgba(36,53,43,0.25)]"
|
||||
} ${className}`}
|
||||
>
|
||||
<span
|
||||
className={`flex-shrink-0 flex items-center justify-center w-11 h-11 border transition-colors ${
|
||||
accent ? "border-honey/40 text-honey-dark" : "border-ink/15 text-forest"
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</span>
|
||||
<span>
|
||||
<span className="block text-lg text-forest">{title}</span>
|
||||
<span className="block mt-1 text-sm text-ink/70">{description}</span>
|
||||
</span>
|
||||
</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-forest/40 text-forest bg-forest/5",
|
||||
ABHOLBEREIT: "border-forest/40 text-forest bg-forest/5",
|
||||
ABGEHOLT: "border-forest/40 text-forest 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. */
|
||||
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 border px-2.5 py-1 text-xs tracking-wide-caps ${style}`}>
|
||||
{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">
|
||||
<span
|
||||
className={`flex-shrink-0 w-2.5 h-2.5 rounded-full ${isDone ? "bg-forest" : "bg-ink/20"}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{i < steps.length - 1 ? (
|
||||
<span
|
||||
className={`ml-1 h-px flex-1 ${isLineDone ? "bg-forest" : "bg-ink/15"}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<p className={`mt-2 text-[11px] text-center leading-tight ${isDone ? "text-forest" : "text-ink/45"}`}>
|
||||
{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-14 text-center">
|
||||
<span className="inline-flex items-center justify-center w-12 h-12 border border-ink/15 text-ink/40 mx-auto">
|
||||
{icon}
|
||||
</span>
|
||||
<p className="mt-4 text-forest">{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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user