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]>
This commit is contained in:
1 parent
b77516b9f5
commit
6950657c91
95 files changed
+514
-272
No files matched your search
@@ -64,7 +64,7 @@ export default async function AdminInquiriesPage(props: PageProps<"/admin/anfrag
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<p className="text-forest">{inquiry.companyName}</p>
|
||||
<p className="text-heading">{inquiry.companyName}</p>
|
||||
<AdminBadge tone={STATUS_TONES[inquiry.status] ?? "neutral"} pulse={inquiry.status === "NEU"}>
|
||||
{statusLabels[inquiry.status] ?? inquiry.status}
|
||||
</AdminBadge>
|
||||
@@ -91,7 +91,7 @@ export default async function AdminInquiriesPage(props: PageProps<"/admin/anfrag
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
className="border border-forest px-3 py-1.5 text-xs text-forest transition-colors duration-300 hover:bg-forest hover:text-cream"
|
||||
className="border border-heading px-3 py-1.5 text-xs text-heading transition-colors duration-300 hover:bg-forest hover:text-cream"
|
||||
>
|
||||
{t.adminInquiriesPage.save}
|
||||
</button>
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function AdminAuditLogPage() {
|
||||
<tr key={entry.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 whitespace-nowrap tabular-nums text-ink/70">{formatDateTime(entry.createdAt)}</td>
|
||||
<td className="p-4 whitespace-nowrap">{entry.adminEmail}</td>
|
||||
<td className="p-4 whitespace-nowrap font-mono text-xs text-forest">{entry.action}</td>
|
||||
<td className="p-4 whitespace-nowrap font-mono text-xs text-heading">{entry.action}</td>
|
||||
<td className="p-4 whitespace-nowrap text-ink/70">
|
||||
{entry.entityType}
|
||||
{entry.entityId ? <span className="text-ink/65"> #{entry.entityId.slice(-6)}</span> : null}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default async function AdminBenutzerEditPage(props: PageProps<"/admin/ben
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<BackLink href="/admin/benutzer" label={tu.backToUsers} />
|
||||
<h1 className="text-2xl text-forest mb-2">
|
||||
<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>
|
||||
@@ -61,7 +61,7 @@ export default async function AdminBenutzerEditPage(props: PageProps<"/admin/ben
|
||||
</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-forest">{tu.securityHeading}</h2>
|
||||
<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>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function AdminBenutzerNewPage() {
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<BackLink href="/admin/benutzer" label={tu.backToUsers} />
|
||||
<h1 className="text-2xl text-forest mb-2">{tu.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-2">{tu.title}</h1>
|
||||
<p className="text-sm text-ink/70 mb-8">{tu.intro}</p>
|
||||
|
||||
<AdminFormPanel action={createCustomerAction} actions={<Button type="submit">{tu.createAndSend}</Button>}>
|
||||
|
||||
@@ -76,7 +76,7 @@ export default async function AdminBenutzerPage(props: PageProps<"/admin/benutze
|
||||
|
||||
return (
|
||||
<tr key={customer.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 text-forest">
|
||||
<td className="p-4 text-heading">
|
||||
{customer.firstName} {customer.lastName}
|
||||
{isSelf ? <span className="text-ink/65">{t.adminUsersPage.youSuffix}</span> : null}
|
||||
</td>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
||||
<div className="flex flex-wrap items-start justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<h1 className="text-2xl text-forest">{order.orderNumber}</h1>
|
||||
<h1 className="text-2xl text-heading">{order.orderNumber}</h1>
|
||||
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
|
||||
</div>
|
||||
<p className="text-sm text-ink/65 mt-1">{formatDate(order.createdAt)}</p>
|
||||
@@ -161,7 +161,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
||||
) : dhlConfigured ? (
|
||||
<form action={createDhlLabelAction}>
|
||||
<input type="hidden" name="orderId" value={order.id} />
|
||||
<button type="submit" className="bg-honey-dark text-cream px-4 py-2 text-sm">
|
||||
<button type="submit" className="bg-honey-deep text-cream px-4 py-2 text-sm">
|
||||
{t.adminOrderDetailPage.dhlCreateButton}
|
||||
</button>
|
||||
</form>
|
||||
@@ -215,7 +215,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
||||
{order.items.map((item) => (
|
||||
<tr key={item.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="py-2">
|
||||
<Link href={`/admin/produkte/${item.productId}`} className="text-forest underline">
|
||||
<Link href={`/admin/produkte/${item.productId}`} className="text-heading underline">
|
||||
{item.productName}
|
||||
</Link>{" "}
|
||||
<span className="text-ink/65">({item.weightGrams} g)</span>
|
||||
@@ -249,7 +249,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
||||
<span>−{formatPrice(order.discountCents, order.currency)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex justify-between text-forest pt-1 border-t border-ink/10">
|
||||
<div className="flex justify-between text-heading pt-1 border-t border-ink/10">
|
||||
<span>{t.adminOrderDetailPage.totalLabel}</span>
|
||||
<span>{formatPrice(order.totalCents, order.currency)}</span>
|
||||
</div>
|
||||
@@ -282,7 +282,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
||||
<span>{order.invoice.invoiceNumber}</span>
|
||||
<a
|
||||
href={`/rechnung/${order.invoice.invoiceNumber}/pdf`}
|
||||
className="link-draw text-sm text-forest"
|
||||
className="link-draw text-sm text-heading"
|
||||
>
|
||||
{t.adminOrderDetailPage.downloadPdf}
|
||||
</a>
|
||||
|
||||
@@ -112,7 +112,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
<tbody className="divide-y divide-ink/10">
|
||||
{orders.map((order) => (
|
||||
<tr key={order.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 text-forest whitespace-nowrap">
|
||||
<td className="p-4 text-heading whitespace-nowrap">
|
||||
<Link href={`/admin/bestellungen/${order.id}`} className="link-draw">
|
||||
{order.orderNumber}
|
||||
</Link>
|
||||
@@ -155,7 +155,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
className="border border-forest px-3 py-1.5 text-xs text-forest transition-colors duration-300 hover:bg-forest hover:text-cream"
|
||||
className="border border-heading px-3 py-1.5 text-xs text-heading transition-colors duration-300 hover:bg-forest hover:text-cream"
|
||||
>
|
||||
{t.adminOrdersPage.save}
|
||||
</button>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default async function AdminBewertungenPage(props: PageProps<"/admin/bewe
|
||||
{reviews.map((review) => (
|
||||
<tr key={review.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4">
|
||||
<Link href={`/honig/${review.product.slug}`} className="link-draw text-forest">
|
||||
<Link href={`/honig/${review.product.slug}`} className="link-draw text-heading">
|
||||
{review.product.name}
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async function AdminBatchEditPage(props: PageProps<"/admin/charge
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<BackLink href="/admin/chargen" label={tb.backToBatches} />
|
||||
<h1 className="text-2xl text-forest mb-2">{tb.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-2">{tb.title}</h1>
|
||||
<p className="text-sm text-ink/65 mb-8">
|
||||
{batch.code} · {batch.harvest.type} {batch.harvest.year}
|
||||
</p>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default async function AdminBatchesPage() {
|
||||
<tbody className="divide-y divide-ink/10">
|
||||
{batches.map((batch) => (
|
||||
<tr key={batch.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 text-forest">{batch.code}</td>
|
||||
<td className="p-4 text-heading">{batch.code}</td>
|
||||
<td className="p-4">
|
||||
{batch.harvest.type} {batch.harvest.year}
|
||||
</td>
|
||||
@@ -81,7 +81,7 @@ export default async function AdminBatchesPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-10 bg-background border border-ink/10 p-6 max-w-xl">
|
||||
<h2 className="font-serif text-xl text-forest mb-4">{t.adminBatchesPage.newBatchHeading}</h2>
|
||||
<h2 className="font-serif text-xl text-heading mb-4">{t.adminBatchesPage.newBatchHeading}</h2>
|
||||
<form action={createBatchAction} className="space-y-4">
|
||||
<div className="grid grid-cols-2 items-end gap-4">
|
||||
<AdminField
|
||||
|
||||
@@ -29,7 +29,7 @@ export default async function AdminHarvestEditPage(props: PageProps<"/admin/ernt
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<BackLink href="/admin/ernten" label={th.backToHarvests} />
|
||||
<h1 className="text-2xl text-forest mb-8">{th.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-8">{th.title}</h1>
|
||||
|
||||
{harvest.isPublished ? null : (
|
||||
<div className="mb-6 flex flex-col justify-between gap-3 border border-honey/40 bg-honey/5 px-4 py-3 sm:flex-row sm:items-center">
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async function AdminHarvestsPage() {
|
||||
<tbody className="divide-y divide-ink/10">
|
||||
{harvests.map((harvest) => (
|
||||
<tr key={harvest.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 text-forest">{harvest.type}</td>
|
||||
<td className="p-4 text-heading">{harvest.type}</td>
|
||||
<td className="p-4 tabular-nums">{harvest.year}</td>
|
||||
<td className="p-4 whitespace-nowrap tabular-nums">{formatDate(harvest.harvestDate)}</td>
|
||||
<td className="p-4">{harvest.location}</td>
|
||||
@@ -93,7 +93,7 @@ export default async function AdminHarvestsPage() {
|
||||
|
||||
<div className="mt-10 max-w-xl">
|
||||
<div className="bg-background border border-ink/10 p-6">
|
||||
<h2 className="font-serif text-xl text-forest mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
|
||||
<h2 className="font-serif text-xl text-heading mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
|
||||
<form action={createHarvestAction} className="space-y-4">
|
||||
<div className="grid grid-cols-2 items-end gap-4">
|
||||
<AdminField name="type" label={t.adminHarvestsPage.typeLabel} required />
|
||||
|
||||
@@ -21,13 +21,13 @@ export default function AdminError({
|
||||
return (
|
||||
<div className="py-20 text-center">
|
||||
<p className="tracking-wide-caps text-xs text-honey-dark mb-3">{t.errorPage.eyebrow}</p>
|
||||
<h1 className="text-2xl text-forest">{t.errorPage.title}</h1>
|
||||
<h1 className="text-2xl text-heading">{t.errorPage.title}</h1>
|
||||
<p className="mt-3 text-ink/70">{t.errorPage.description}</p>
|
||||
<div className="mt-6 flex items-center justify-center gap-4">
|
||||
<Button type="button" onClick={() => reset()}>
|
||||
{t.errorPage.retry}
|
||||
</Button>
|
||||
<Link href="/admin" className="text-sm text-forest underline">
|
||||
<Link href="/admin" className="text-sm text-heading underline">
|
||||
{t.adminVouchersPage.backToDashboard}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default async function AdminGutscheinEditPage(props: PageProps<"/admin/gu
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<BackLink href="/admin/gutscheine" label={tv.backToVouchers} />
|
||||
<h1 className="text-2xl text-forest mb-2">{tv.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-2">{tv.title}</h1>
|
||||
<p className="text-sm text-ink/65 mb-8">{tv.redeemedSoFar(code.usedCount, code.maxUses)}</p>
|
||||
|
||||
<AdminFormPanel action={updateDiscountCodeAction} actions={<Button type="submit">{tv.save}</Button>}>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default async function AdminGutscheinNewPage() {
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<BackLink href="/admin/gutscheine" label={tv.backToVouchers} />
|
||||
<h1 className="text-2xl text-forest mb-8">{tv.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-8">{tv.title}</h1>
|
||||
|
||||
<AdminFormPanel action={createDiscountCodeAction} actions={<Button type="submit">{tv.create}</Button>}>
|
||||
<AdminFieldset legend={tv.sectionDiscount}>
|
||||
|
||||
@@ -42,7 +42,7 @@ export default async function AdminGutscheinePage() {
|
||||
<tbody className="divide-y divide-ink/10">
|
||||
{codes.map((code) => (
|
||||
<tr key={code.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 text-forest font-mono">{code.code}</td>
|
||||
<td className="p-4 text-heading font-mono">{code.code}</td>
|
||||
<td className="p-4">
|
||||
{code.type === "PERCENT" ? `${code.value} %` : formatPrice(code.value)}
|
||||
</td>
|
||||
|
||||
@@ -72,7 +72,7 @@ function FieldEditor({
|
||||
<select
|
||||
name={def.key}
|
||||
defaultValue={current}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-heading"
|
||||
>
|
||||
{(def.options ?? []).map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
@@ -92,7 +92,7 @@ function FieldEditor({
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-heading"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@@ -100,7 +100,7 @@ function FieldEditor({
|
||||
name={def.key}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-forest"
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 focus:outline-none focus:border-heading"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
@@ -120,7 +120,7 @@ function FieldEditor({
|
||||
rows={rows}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@@ -128,7 +128,7 @@ function FieldEditor({
|
||||
name={`de::${def.key}`}
|
||||
defaultValue={row?.valueDe ?? ""}
|
||||
placeholder={def.getDefault("de")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
@@ -140,7 +140,7 @@ function FieldEditor({
|
||||
rows={rows}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
@@ -148,7 +148,7 @@ function FieldEditor({
|
||||
name={`en::${def.key}`}
|
||||
defaultValue={row?.valueEn ?? ""}
|
||||
placeholder={def.getDefault("en")}
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-forest"
|
||||
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2 text-sm focus:outline-none focus:border-heading"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
@@ -185,7 +185,7 @@ function GroupForm({
|
||||
className="mb-10 bg-background border border-ink/10 p-6 space-y-6"
|
||||
>
|
||||
<input type="hidden" name="group" value={groupKeyValue} />
|
||||
<h3 className="text-lg text-forest">{groupHeading}</h3>
|
||||
<h3 className="text-lg text-heading">{groupHeading}</h3>
|
||||
|
||||
{fields.map((def) => (
|
||||
<FieldEditor key={def.key} def={def} row={current.get(def.key)} locale={locale} t={t} />
|
||||
@@ -216,11 +216,11 @@ export default async function AdminInhaltePage() {
|
||||
return (
|
||||
<div>
|
||||
<BackLink href="/admin" label={t.adminContentPage.backToDashboard} />
|
||||
<h1 className="text-2xl text-forest mb-2">{t.adminContentPage.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-2">{t.adminContentPage.title}</h1>
|
||||
<p className="text-sm text-ink/65 mb-8 max-w-2xl">{t.adminContentPage.intro}</p>
|
||||
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xl text-forest mb-1">{t.adminContentPage.sectionImages}</h2>
|
||||
<h2 className="text-xl text-heading mb-1">{t.adminContentPage.sectionImages}</h2>
|
||||
<p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionImagesIntro}</p>
|
||||
{groups.map((group) => (
|
||||
<GroupForm
|
||||
@@ -236,7 +236,7 @@ export default async function AdminInhaltePage() {
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-xl text-forest mb-1">{t.adminContentPage.sectionText}</h2>
|
||||
<h2 className="text-xl text-heading mb-1">{t.adminContentPage.sectionText}</h2>
|
||||
<p className="text-sm text-ink/65 mb-6 max-w-2xl">{t.adminContentPage.sectionTextIntro}</p>
|
||||
{groups.map((group) => (
|
||||
<GroupForm
|
||||
|
||||
@@ -8,6 +8,8 @@ import { AdminShell } from "@/components/admin-shell";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
import { getBrandAssets } from "@/lib/brand";
|
||||
import { cookies } from "next/headers";
|
||||
import { THEME_COOKIE, parseThemeChoice } from "@/lib/theme";
|
||||
|
||||
// Admin-Daten (Bestellungen, Bestand) muessen immer aktuell sein; zudem
|
||||
// entkoppelt das den Docker-Build-Schritt von einer DB-Verbindung.
|
||||
@@ -49,6 +51,7 @@ export default async function AdminLayout({ children }: LayoutProps<"/admin">) {
|
||||
// abgeholte Bestellungen (keine offenen Zahlungen), neue Retouren,
|
||||
// unfreigegebene Bewertungen, neue Firmenanfragen. Reine COUNT-Abfragen.
|
||||
const brand = await getBrandAssets();
|
||||
const themeChoice = parseThemeChoice((await cookies()).get(THEME_COOKIE)?.value);
|
||||
const [openOrders, openReturns, pendingReviews, newInquiries] = await Promise.all([
|
||||
prisma.order.count({
|
||||
where: { status: { in: ["BEZAHLT", "IN_VORBEREITUNG", "VERSANDBEREIT", "ABHOLBEREIT"] }, archivedAt: null },
|
||||
@@ -62,6 +65,7 @@ export default async function AdminLayout({ children }: LayoutProps<"/admin">) {
|
||||
<AdminShell
|
||||
adminName={`${admin.firstName} ${admin.lastName}`}
|
||||
logoSrc={brand.logoInverted}
|
||||
themeChoice={themeChoice}
|
||||
badges={{
|
||||
"/admin/bestellungen": openOrders,
|
||||
"/admin/retouren": openReturns,
|
||||
|
||||
+16
-16
@@ -303,7 +303,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
{/* Begruessung statt nackter Ueberschrift: der Tag beginnt hier. */}
|
||||
<div className="mb-10">
|
||||
<p className="text-sm text-ink/60">{todayLine}</p>
|
||||
<h1 className="mt-1 text-4xl md:text-5xl leading-tight text-forest">
|
||||
<h1 className="mt-1 text-4xl md:text-5xl leading-tight text-heading">
|
||||
{t.adminDashboardPage.greeting(dayPart, admin?.firstName ?? "")}
|
||||
</h1>
|
||||
</div>
|
||||
@@ -396,23 +396,23 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
{/* Was jetzt ansteht — direkt anklickbar. Wartet nichts, gibt es eine
|
||||
kleine Belohnung statt einer leeren Liste. */}
|
||||
<section className="border border-ink/10 bg-background p-6 lg:col-span-2">
|
||||
<h2 className="text-lg text-forest">{t.adminDashboardPage.toDoHeading}</h2>
|
||||
<h2 className="text-lg text-heading">{t.adminDashboardPage.toDoHeading}</h2>
|
||||
{toDo.length > 0 ? (
|
||||
<ul className="mt-4 divide-y divide-ink/10">
|
||||
{toDo.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="group flex items-center gap-3 py-3 text-sm text-ink/80 transition-colors hover:text-forest"
|
||||
className="group flex items-center gap-3 py-3 text-sm text-ink/80 transition-colors hover:text-heading"
|
||||
>
|
||||
<span className="flex h-7 min-w-7 items-center justify-center rounded-full bg-honey-dark px-2 text-xs font-semibold text-cream tabular-nums">
|
||||
<span className="flex h-7 min-w-7 items-center justify-center rounded-full bg-honey-deep px-2 text-xs font-semibold text-cream tabular-nums">
|
||||
{item.count}
|
||||
</span>
|
||||
<span className="flex-1">{item.label}</span>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4 text-ink/40 transition-[color,transform] duration-300 group-hover:translate-x-1 group-hover:text-forest"
|
||||
className="h-4 w-4 text-ink/40 transition-[color,transform] duration-300 group-hover:translate-x-1 group-hover:text-heading"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
@@ -430,7 +430,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
<path d="M7.5 13.2l3 3 6-6.4" stroke="var(--color-cream)" strokeWidth="1.8" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="font-serif text-xl text-forest">{t.adminDashboardPage.allDoneTitle}</p>
|
||||
<p className="font-serif text-xl text-heading">{t.adminDashboardPage.allDoneTitle}</p>
|
||||
<p className="mt-1 text-sm text-ink/65">{t.adminDashboardPage.allDoneText}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -448,11 +448,11 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
key={stat.key}
|
||||
className="group border border-ink/10 bg-background p-5 transition-[border-color,transform] duration-300 hover:-translate-y-0.5 hover:border-honey/50"
|
||||
>
|
||||
<span className="flex h-9 w-9 items-center justify-center border border-ink/10 text-forest transition-colors group-hover:border-honey/50 group-hover:text-honey-dark">
|
||||
<span className="flex h-9 w-9 items-center justify-center border border-ink/10 text-heading transition-colors group-hover:border-honey/50 group-hover:text-honey-dark">
|
||||
<Icon />
|
||||
</span>
|
||||
<p className="mt-4 text-xs text-ink/60">{statLabels[stat.key]}</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-forest">{stat.value}</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-heading">{stat.value}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -460,7 +460,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
|
||||
<div className="mt-10 grid lg:grid-cols-2 gap-6">
|
||||
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
|
||||
<h2 className="text-lg text-forest mb-4">{t.adminDashboardPage.topProductsHeading}</h2>
|
||||
<h2 className="text-lg text-heading mb-4">{t.adminDashboardPage.topProductsHeading}</h2>
|
||||
{topProducts.length === 0 ? (
|
||||
<p className="text-sm text-ink/65">{t.adminDashboardPage.noSalesYet}</p>
|
||||
) : (
|
||||
@@ -469,13 +469,13 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
<li key={product.name} className="flex items-center justify-between gap-4">
|
||||
<span className="flex items-center gap-3 text-ink/80">
|
||||
<span className="relative flex h-6 w-6 flex-shrink-0 items-center justify-center text-[10px] font-semibold text-cream">
|
||||
<HexMark className={`absolute inset-0 h-6 w-6 ${i === 0 ? "text-[var(--color-chart-honey)]" : "text-forest/70"}`} />
|
||||
<HexMark className={`absolute inset-0 h-6 w-6 ${i === 0 ? "text-[var(--color-chart-honey)]" : "text-heading/70"}`} />
|
||||
<span className="relative">{i + 1}</span>
|
||||
</span>
|
||||
{product.name}{" "}
|
||||
<span className="text-ink/65">{t.adminDashboardPage.unitsSuffix(product.quantity)}</span>
|
||||
</span>
|
||||
<span className="text-forest whitespace-nowrap">{formatPrice(product.revenueCents)}</span>
|
||||
<span className="text-heading whitespace-nowrap">{formatPrice(product.revenueCents)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -483,7 +483,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
</div>
|
||||
|
||||
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
|
||||
<h2 className="text-lg text-forest mb-4">
|
||||
<h2 className="text-lg text-heading mb-4">
|
||||
{t.adminDashboardPage.revenueByCategoryHeading}
|
||||
</h2>
|
||||
{revenueByCategory.length === 0 ? (
|
||||
@@ -496,7 +496,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
<span className="text-ink/70">
|
||||
{t.common.categories[category as keyof typeof t.common.categories] ?? category}
|
||||
</span>
|
||||
<span className="text-forest">{formatPrice(totals.revenueCents)}</span>
|
||||
<span className="text-heading">{formatPrice(totals.revenueCents)}</span>
|
||||
</div>
|
||||
{/* Meter: Spur ist eine hellere Stufe desselben Tons. */}
|
||||
<div className="h-2 overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--color-chart-honey)_15%,transparent)]">
|
||||
@@ -514,7 +514,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
|
||||
<div className="mt-6 grid lg:grid-cols-2 gap-6">
|
||||
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
|
||||
<h2 className="text-lg text-forest mb-4">
|
||||
<h2 className="text-lg text-heading mb-4">
|
||||
{t.adminDashboardPage.revenueTrendHeading(REVENUE_TREND_DAYS)}
|
||||
</h2>
|
||||
{/* Balken nach dataviz-Skill: duenn, 4px gerundete Kappe an der
|
||||
@@ -553,7 +553,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
</div>
|
||||
|
||||
<div className="bg-background border border-ink/10 p-6 transition-colors hover:border-ink/20">
|
||||
<h2 className="text-lg text-forest mb-4">{t.adminDashboardPage.topVouchersHeading}</h2>
|
||||
<h2 className="text-lg text-heading mb-4">{t.adminDashboardPage.topVouchersHeading}</h2>
|
||||
{topDiscountCodes.length === 0 ? (
|
||||
<p className="text-sm text-ink/65">{t.adminDashboardPage.noVouchersYet}</p>
|
||||
) : (
|
||||
@@ -566,7 +566,7 @@ export default async function AdminDashboardPage(props: PageProps<"/admin">) {
|
||||
({code.type === "PERCENT" ? `${code.value} %` : formatPrice(code.value)})
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-forest whitespace-nowrap">
|
||||
<span className="text-heading whitespace-nowrap">
|
||||
{t.adminDashboardPage.usedCountLabel(code.usedCount, code.maxUses)}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default async function AdminProductEditPage(props: PageProps<"/admin/prod
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<BackLink href="/admin/produkte" label={te.backToProducts} />
|
||||
<h1 className="text-2xl text-forest mb-2">{te.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-2">{te.title}</h1>
|
||||
<p className="text-sm text-ink/65 mb-8">
|
||||
{t.common.categories[product.category]}
|
||||
{product.harvest && product.batch
|
||||
|
||||
@@ -26,7 +26,7 @@ export default async function AdminProductNewPage() {
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<BackLink href="/admin/produkte" label={tn.backToProducts} />
|
||||
<h1 className="text-2xl text-forest mb-8">{tn.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-8">{tn.title}</h1>
|
||||
<AdminFormPanel
|
||||
action={createProductAction}
|
||||
className="group/product"
|
||||
|
||||
@@ -99,7 +99,7 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
|
||||
<div key={request.id} className="bg-background border border-ink/10 p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-forest">{request.order.orderNumber}</p>
|
||||
<p className="text-heading">{request.order.orderNumber}</p>
|
||||
<p className="text-sm text-ink/70">
|
||||
{request.order.firstName} {request.order.lastName} · {request.order.email}
|
||||
</p>
|
||||
@@ -136,7 +136,7 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-2 text-forest tabular-nums">{formatPrice(request.order.totalCents, request.order.currency)}</p>
|
||||
<p className="mt-2 text-heading tabular-nums">{formatPrice(request.order.totalCents, request.order.currency)}</p>
|
||||
|
||||
<p className="mt-4 text-sm text-ink/70 border-t border-ink/10 pt-4">{request.reason}</p>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default async function AdminVersandEditPage(props: PageProps<"/admin/vers
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<BackLink href="/admin/versand" label={ts.backToShipping} />
|
||||
<h1 className="text-2xl text-forest mb-8">{ts.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-8">{ts.title}</h1>
|
||||
|
||||
<AdminFormPanel action={updateShippingTierAction} actions={<Button type="submit">{ts.save}</Button>}>
|
||||
<input type="hidden" name="id" value={tier.id} />
|
||||
|
||||
@@ -12,7 +12,7 @@ export default async function AdminVersandNewPage() {
|
||||
return (
|
||||
<div className="max-w-xl">
|
||||
<BackLink href="/admin/versand" label={ts.backToShipping} />
|
||||
<h1 className="text-2xl text-forest mb-8">{ts.title}</h1>
|
||||
<h1 className="text-2xl text-heading mb-8">{ts.title}</h1>
|
||||
|
||||
<AdminFormPanel action={createShippingTierAction} actions={<Button type="submit">{ts.create}</Button>}>
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function AdminVersandPage() {
|
||||
<tbody className="divide-y divide-ink/10">
|
||||
{tiers.map((tier) => (
|
||||
<tr key={tier.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 text-forest">
|
||||
<td className="p-4 text-heading">
|
||||
{tier.maxQuantity === null
|
||||
? t.adminShippingPage.quantityFrom(tier.minQuantity)
|
||||
: t.adminShippingPage.quantityRange(tier.minQuantity, tier.maxQuantity)}
|
||||
|
||||
Reference in new issue
Block a user