Compare commits

...
8 Commits
Author SHA1 Message Date
Nick SzaunigandClaude Opus 5.5 fdb6456207 feat(ui): Leerer Warenkorb zeigt verfuegbare Honige
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m24s
Bisher stand dort nur ein Satz und ein Knopf zum Shop. Jetzt steht der
Hinweis als Spalte links, daneben bis zu zwei aktuell verfuegbare
Produkte (Honig zuerst) - dasselbe Muster wie "Aktuelle Ernte" auf der
Startseite. Nutzt die bereits geladenen availableProducts.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 02:14:52 +02:00
Nick SzaunigandClaude Opus 5.5 0fb176add8 feat(ui): Microsoft-Anmeldung nur wenn eingerichtet, in der Anmelde-Spalte
Der Knopf stand unter beiden Spalten (Anmelden und Konto erstellen),
obwohl er zur Anmeldung gehoert, und erschien auch ohne eingerichtete
Entra-Konfiguration - der Klick endete dann in der Meldung "SSO ist noch
nicht konfiguriert". Jetzt steht er unter dem Anmeldeformular und nur,
wenn isEntraConfigured() zutrifft. Die serverseitige Pruefung in der
Login-Route bleibt als zweite Absicherung.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 02:13:19 +02:00
Nick SzaunigandClaude Opus 5.5 774bc9cf00 feat(ui): Pakete fuer Firmenkunden als Auswahl-Karten im Formular
Die drei Pakete standen doppelt: als Deko-Kacheln (mit grossem
Waben-Wasserzeichen) ueber dem Formular und darin noch einmal als
Auswahlliste. Jetzt waehlt man sie direkt im Formular als Karten mit
sichtbarem Auswahl- und Fokuszustand; die Kacheln entfallen. Feldname
und Werte (package = KLASSIK/GESCHENK/INDIVIDUELL) sind unveraendert.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 02:11:29 +02:00
Nick SzaunigandClaude Opus 5.5 b588975317 feat(ui): Seitenabschluesse, Wissens-Verzeichnis, Kopfzeile ohne Ueberlauf
- Imkerei, Herkunft und Ernten endeten mit dem letzten Absatz direkt vor
  der Fusszeile. Neuer PageOutro: zwei naechste Wege als grosse
  Serif-Links mit Kurztext (Ernten/Shop bzw. Herkunft/Shop).
- Kopfzeile: bei 1280 px lief sie 54 px ueber (horizontale Scrollleiste),
  weil die nicht umbrechbare Slogan-Zeile unter dem Logo schon ab xl
  erschien. Jetzt erst ab 2xl; geprueft 1024 bis 1920 px ohne Ueberlauf.
- Wissen: Inhaltsverzeichnis statt zehn gleicher Bloecke mit je einem
  "Weiterlesen" - ganze Zeile ist der Link, Titel und Kurztext
  nebeneinander. Ungenutzter Schluessel readMore entfernt.
- Produktseite: bei ein, zwei Empfehlungen steht die Ueberschrift als
  erste Rasterspalte statt ueber einer einzelnen verlorenen Karte.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 02:05:25 +02:00
Nick SzaunigandClaude Opus 5.5 908afebacb feat(ui): Produktbild ab Tablet im Hochformat
Die Galerie lief im Querformat 4:3 neben einer langen Detailspalte und
wirkte daneben schmaechtig. Ab md jetzt 4:5, das Glas traegt die Seite;
auf dem Handy bleibt 4:3, damit der Kaufen-Knopf oben bleibt.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 01:47:47 +02:00
Nick SzaunigandClaude Opus 5.5 7c305a3fce feat(ui): Bild-Platzhalter im Stil des Glas-Etiketts
Fehlt ein Foto, zeigte die Seite einen Farbverlauf mit Diagonallinien -
ein generisches Muster, das auf Produkt- und Inhaltskacheln wie
unfertige Vorlage wirkte. Jetzt: ruhige Markenflaeche mit leisem
Wabenraster, das gezeichnete Waben-Zeichen und darunter der Name in
gesperrten Serif-Versalien, wie auf dem Etikett. In kleinen Groessen
(Warenkorb, Galerie-Vorschau) bleibt per Container Query nur das Zeichen.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 01:46:59 +02:00
Nick SzaunigandClaude Opus 5.5 7e467a9ddf feat(ui): Startseite mit eigenstaendigen Abschnitten statt Schablonen-Zickzack
- Aktuelle Ernte: bei ein, zwei Produkten steht die Ueberschrift als
  eigene Spalte daneben, statt ueber einem halb leeren Dreierraster.
- Herkunft: Foto randlos ueber die halbe Seitenbreite, Text daneben,
  ohne Rahmen.
- Philosophie: typografische Reihe mit Trennlinien und grossen
  Serif-Titeln statt vier Icon-Karten.
- Region: breites Bild, der Text liegt als Tafel ueberlappend darueber.
- Geschaeftskunden: kompakte Tafel mit randlosem Bild statt eines dritten
  grossen Bild/Text-Wechsels.

Inhalte, Texte und Content-Schluessel bleiben unveraendert.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 01:45:06 +02:00
Nick SzaunigandClaude Opus 5.5 1fa8ca246a refactor(ui): Kicker-Zeilen ueber Ueberschriften entfernt
Jede zweite Ueberschrift trug eine kleine Versalien-Zeile mit Wabenpunkt
("• STIMMEN", "• AKTUELL VERFUEGBAR", "• KUNDENKONTO" ...). Auf 29 Seiten
und Abschnitten wiederholt las sich das wie eine Vorlage. SectionHeading
rendert sie nicht mehr, die Prop ist entfernt; die Ueberschrift traegt den
Abschnitt allein.

Die eyebrow-Texte, die weiterhin als Seitentitel, Bildbeschriftung oder
Breadcrumb dienen, bleiben; 18 danach ungenutzte Eintraege sind aus Typ und
beiden Woerterbuechern entfernt.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 01:41:48 +02:00
39 changed files with 305 additions and 287 deletions

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
return ( return (
<div className="py-20"> <div className="py-20">
<Container className="max-w-xl"> <Container className="max-w-xl">
<SectionHeading eyebrow={t.trackOrderPage.eyebrow} title={t.trackOrderPage.title} /> <SectionHeading title={t.trackOrderPage.title} />
<p className="mt-4 text-ink/70 leading-relaxed">{t.trackOrderPage.intro}</p> <p className="mt-4 text-ink/70 leading-relaxed">{t.trackOrderPage.intro}</p>
<FormPanel className="mt-8"> <FormPanel className="mt-8">
-1
View File
@@ -27,7 +27,6 @@ export default async function CheckoutCancelPage(props: PageProps<"/checkout/can
<div className="py-24"> <div className="py-24">
<Container className="max-w-xl text-center"> <Container className="max-w-xl text-center">
<SectionHeading <SectionHeading
eyebrow={t.checkoutCancelPage.eyebrow}
title={t.checkoutCancelPage.title} title={t.checkoutCancelPage.title}
align="center" align="center"
className="mx-auto" className="mx-auto"
+2 -2
View File
@@ -295,7 +295,7 @@ export function CheckoutForm({
return ( return (
<div className="py-20"> <div className="py-20">
<Container> <Container>
<SectionHeading eyebrow={t.checkoutPage.eyebrow} title={t.checkoutPage.emptyCart} /> <SectionHeading title={t.checkoutPage.emptyCart} />
</Container> </Container>
</div> </div>
); );
@@ -328,7 +328,7 @@ export function CheckoutForm({
{/* Stripe zeigt im Zahlungsschritt selbst Positionen und Summe — {/* Stripe zeigt im Zahlungsschritt selbst Positionen und Summe —
Formular und eigene Zusammenfassung treten dann zurueck. */} Formular und eigene Zusammenfassung treten dann zurueck. */}
<form onSubmit={handleSubmit} hidden={payment !== null} className="lg:col-span-2 space-y-8"> <form onSubmit={handleSubmit} hidden={payment !== null} className="lg:col-span-2 space-y-8">
<SectionHeading eyebrow={t.checkoutPage.eyebrow} title={t.checkoutPage.yourDetails} /> <SectionHeading title={t.checkoutPage.yourDetails} />
<div className="grid sm:grid-cols-2 gap-4"> <div className="grid sm:grid-cols-2 gap-4">
<Field <Field
+1 -3
View File
@@ -41,7 +41,7 @@ export default async function CheckoutSuccessPage(props: PageProps<"/checkout/su
return ( return (
<div className="py-16"> <div className="py-16">
<Container className="max-w-3xl"> <Container className="max-w-3xl">
<SectionHeading eyebrow={t.checkoutSuccessPage.openEyebrow} title={t.checkoutSuccessPage.openTitle} /> <SectionHeading title={t.checkoutSuccessPage.openTitle} />
<p role="status" className="mt-4 mb-10 text-ink/70"> <p role="status" className="mt-4 mb-10 text-ink/70">
{t.checkoutSuccessPage.openText} {t.checkoutSuccessPage.openText}
</p> </p>
@@ -64,7 +64,6 @@ export default async function CheckoutSuccessPage(props: PageProps<"/checkout/su
<div className="py-24"> <div className="py-24">
<Container className="max-w-xl text-center"> <Container className="max-w-xl text-center">
<SectionHeading <SectionHeading
eyebrow={t.checkoutSuccessPage.notCompletedEyebrow}
title={t.checkoutSuccessPage.notCompletedTitle} title={t.checkoutSuccessPage.notCompletedTitle}
align="center" align="center"
className="mx-auto" className="mx-auto"
@@ -88,7 +87,6 @@ export default async function CheckoutSuccessPage(props: PageProps<"/checkout/su
<ClearCartOnMount /> <ClearCartOnMount />
<Container className="max-w-xl text-center"> <Container className="max-w-xl text-center">
<SectionHeading <SectionHeading
eyebrow={isPaid ? t.checkoutSuccessPage.successEyebrow : t.checkoutSuccessPage.pendingEyebrow}
title={isPaid ? t.checkoutSuccessPage.successTitle : t.checkoutSuccessPage.pendingTitle} title={isPaid ? t.checkoutSuccessPage.successTitle : t.checkoutSuccessPage.pendingTitle}
align="center" align="center"
className="mx-auto" className="mx-auto"
+9 -1
View File
@@ -8,6 +8,7 @@ import { getHarvests } from "@/lib/data";
import { formatMonthYear } from "@/lib/format"; import { formatMonthYear } from "@/lib/format";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { PageOutro } from "@/components/page-outro";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Unsere Ernten", title: "Unsere Ernten",
@@ -32,7 +33,7 @@ export default async function ErntenPage() {
<div className="py-20"> <div className="py-20">
<Container> <Container>
<ScrollReveal> <ScrollReveal>
<SectionHeading level="h1" eyebrow={t.erntenPage.eyebrow} title={t.erntenPage.title} /> <SectionHeading level="h1" title={t.erntenPage.title} />
</ScrollReveal> </ScrollReveal>
<div className="mt-16 space-y-24"> <div className="mt-16 space-y-24">
@@ -96,6 +97,13 @@ export default async function ErntenPage() {
) : null} ) : null}
</div> </div>
</Container> </Container>
<PageOutro
ariaLabel={t.pageOutro.ariaLabel}
links={[
{ href: "/herkunft", label: t.nav.origin, text: t.pageOutro.originText },
{ href: "/shop", label: t.nav.shop, text: t.pageOutro.shopText },
]}
/>
</div> </div>
); );
} }
-1
View File
@@ -43,7 +43,6 @@ export default function Error({ error, reset }: { error: Error & { digest?: stri
/> />
</svg> </svg>
<SectionHeading <SectionHeading
eyebrow={t.siteErrorPage.eyebrow}
title={t.siteErrorPage.title} title={t.siteErrorPage.title}
align="center" align="center"
className="mx-auto" className="mx-auto"
+8 -1
View File
@@ -6,6 +6,7 @@ import { ScrollReveal } from "@/components/scroll-reveal";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { getContentValue } from "@/lib/content"; import { getContentValue } from "@/lib/content";
import { PageOutro } from "@/components/page-outro";
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
@@ -44,7 +45,6 @@ export default async function HerkunftPage() {
<div className="max-w-3xl bg-background px-8 pt-10 pb-2 md:px-12 md:pt-12 shadow-[0_-20px_40px_-30px_rgba(36,53,43,0.35)]"> <div className="max-w-3xl bg-background px-8 pt-10 pb-2 md:px-12 md:pt-12 shadow-[0_-20px_40px_-30px_rgba(36,53,43,0.35)]">
<SectionHeading <SectionHeading
level="h1" level="h1"
eyebrow={t.herkunftPage.eyebrow}
title={t.herkunftPage.title} title={t.herkunftPage.title}
subtitle={t.herkunftPage.subtitle} subtitle={t.herkunftPage.subtitle}
/> />
@@ -73,6 +73,13 @@ export default async function HerkunftPage() {
{t.herkunftPage.privacyNote} {t.herkunftPage.privacyNote}
</p> </p>
</Container> </Container>
<PageOutro
ariaLabel={t.pageOutro.ariaLabel}
links={[
{ href: "/ernten", label: t.nav.harvests, text: t.pageOutro.harvestsText },
{ href: "/shop", label: t.nav.shop, text: t.pageOutro.shopText },
]}
/>
</div> </div>
); );
} }
+8 -5
View File
@@ -525,15 +525,18 @@ export default async function ProductDetailPage(
</Container> </Container>
{relatedProducts.length > 0 ? ( {relatedProducts.length > 0 ? (
<Container className="mt-20"> // Bei ein, zwei Empfehlungen steht die Ueberschrift als erste Spalte im
<ScrollReveal> // Raster (wie "Aktuelle Ernte" auf der Startseite) — sonst stuende eine
<h2 className="text-3xl md:text-4xl text-forest mb-10">{t.productPage.relatedProductsHeading}</h2> // einzelne Karte verloren unter einer breiten Ueberschrift.
<Container className="mt-20 grid grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2 lg:grid-cols-3">
<ScrollReveal
className={relatedProducts.length <= 2 ? "sm:col-span-2 lg:col-span-1" : "sm:col-span-2 lg:col-span-3"}
>
<h2 className="text-3xl md:text-4xl text-forest text-balance">{t.productPage.relatedProductsHeading}</h2>
</ScrollReveal> </ScrollReveal>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14">
{relatedProducts.map((related, index) => ( {relatedProducts.map((related, index) => (
<ProductCard key={related.id} product={related} index={index} /> <ProductCard key={related.id} product={related} index={index} />
))} ))}
</div>
</Container> </Container>
) : null} ) : null}
+8 -1
View File
@@ -5,6 +5,7 @@ import { ScrollReveal } from "@/components/scroll-reveal";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { getContentValue } from "@/lib/content"; import { getContentValue } from "@/lib/content";
import { PageOutro } from "@/components/page-outro";
export async function generateMetadata(): Promise<Metadata> { export async function generateMetadata(): Promise<Metadata> {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
@@ -25,7 +26,6 @@ export default async function ImkereiPage() {
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading
level="h1" level="h1"
eyebrow={t.imkereiPage.eyebrow}
title={t.imkereiPage.title} title={t.imkereiPage.title}
/> />
<div className="mt-8 max-w-[62ch] space-y-5 text-ink/75 leading-relaxed"> <div className="mt-8 max-w-[62ch] space-y-5 text-ink/75 leading-relaxed">
@@ -56,6 +56,13 @@ export default async function ImkereiPage() {
/> />
</FramedMedia> </FramedMedia>
</Container> </Container>
<PageOutro
ariaLabel={t.pageOutro.ariaLabel}
links={[
{ href: "/ernten", label: t.nav.harvests, text: t.pageOutro.harvestsText },
{ href: "/shop", label: t.nav.shop, text: t.pageOutro.shopText },
]}
/>
</div> </div>
); );
} }
+1 -1
View File
@@ -22,7 +22,7 @@ export default async function KontaktPage() {
<div className="py-20"> <div className="py-20">
<Container className="max-w-5xl"> <Container className="max-w-5xl">
<ScrollReveal> <ScrollReveal>
<SectionHeading level="h1" eyebrow={t.kontaktPage.eyebrow} title={t.kontaktPage.title} /> <SectionHeading level="h1" title={t.kontaktPage.title} />
</ScrollReveal> </ScrollReveal>
<div className="mt-12 grid gap-6 md:grid-cols-5"> <div className="mt-12 grid gap-6 md:grid-cols-5">
+1 -1
View File
@@ -25,7 +25,7 @@ export default async function KontoAdressenPage() {
<div className="py-20"> <div className="py-20">
<Container className="max-w-3xl"> <Container className="max-w-3xl">
<BackLink href="/konto" label={t.ordersPage.backToAccount} /> <BackLink href="/konto" label={t.ordersPage.backToAccount} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.addressesPage.title} /> <SectionHeading title={t.addressesPage.title} />
<div className="mt-10 grid gap-4 sm:grid-cols-2"> <div className="mt-10 grid gap-4 sm:grid-cols-2">
@@ -45,7 +45,7 @@ export default async function RetourePage(props: PageProps<"/konto/bestellungen/
<div className="py-20"> <div className="py-20">
<Container className="max-w-lg"> <Container className="max-w-lg">
<BackLink href="/konto/bestellungen" label={t.retourePage.backLabel} /> <BackLink href="/konto/bestellungen" label={t.retourePage.backLabel} />
<SectionHeading eyebrow={t.retourePage.orderEyebrow(order.orderNumber)} title={t.retourePage.title} /> <SectionHeading title={t.retourePage.title} />
<FormPanel className="mt-8"> <FormPanel className="mt-8">
<ul className="divide-y divide-ink/10 border-y border-ink/10 text-sm text-ink/75"> <ul className="divide-y divide-ink/10 border-y border-ink/10 text-sm text-ink/75">
+1 -1
View File
@@ -39,7 +39,7 @@ export default async function KontoBestellungenPage() {
<div className="py-20"> <div className="py-20">
<Container className="max-w-3xl"> <Container className="max-w-3xl">
<BackLink href="/konto" label={t.ordersPage.backToAccount} /> <BackLink href="/konto" label={t.ordersPage.backToAccount} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.ordersPage.title} /> <SectionHeading title={t.ordersPage.title} />
{orders.length === 0 ? ( {orders.length === 0 ? (
+29 -31
View File
@@ -8,6 +8,7 @@ import { ConfirmForm } from "@/components/confirm-form";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
import { getCurrentCustomer } from "@/lib/auth"; import { getCurrentCustomer } from "@/lib/auth";
import { isEntraConfigured } from "@/lib/entra";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { import {
registerAction, registerAction,
@@ -30,9 +31,10 @@ export default async function KontoPage() {
return ( return (
<div className="py-20"> <div className="py-20">
<Container className="max-w-3xl"> <Container className="max-w-3xl">
<SectionHeading eyebrow={t.account.eyebrow} title={t.account.loginRegisterTitle} /> <SectionHeading title={t.account.loginRegisterTitle} />
<div className="mt-10 grid md:grid-cols-2 md:divide-x md:divide-ink/10 gap-12 md:gap-0"> <div className="mt-10 grid md:grid-cols-2 md:divide-x md:divide-ink/10 gap-12 md:gap-0">
<form action={loginAction} className="space-y-4 md:pr-12"> <div className="md:pr-12">
<form action={loginAction} className="space-y-4">
<h2 className="text-3xl text-forest">{t.account.login}</h2> <h2 className="text-3xl text-forest">{t.account.login}</h2>
<Field name="email" label={t.account.emailLabel} type="email" required autoComplete="email" /> <Field name="email" label={t.account.emailLabel} type="email" required autoComplete="email" />
<PasswordField name="password" label={t.account.passwordLabel} required autoComplete="current-password" /> <PasswordField name="password" label={t.account.passwordLabel} required autoComplete="current-password" />
@@ -53,6 +55,30 @@ export default async function KontoPage() {
</Link> </Link>
</div> </div>
</form> </form>
{/* Microsoft-Anmeldung nur, wenn sie eingerichtet ist (ENTRA_*,
siehe .env.example) — ein Knopf, der nur "SSO ist noch nicht
konfiguriert" meldet, ist fuer Kundschaft schlechter als keiner.
Sie gehoert zur Anmeldung, steht deshalb in dieser Spalte.
Bewusst ein einfaches <a> statt next/link: der Klick muss eine
echte Top-Level-Navigation ausloesen, die dem Redirect bis zu
login.microsoftonline.com folgt; ein clientseitiger fetch()
wuerde von der connect-src-CSP in proxy.ts blockiert. */}
{isEntraConfigured() ? (
<>
<div className="mt-8 flex items-center gap-4 text-xs text-ink/70">
<span className="h-px flex-1 bg-ink/10" />
{t.account.ssoDivider}
<span className="h-px flex-1 bg-ink/10" />
</div>
<a
href="/api/auth/entra/login"
className="mt-6 inline-flex items-center justify-center gap-2 px-7 py-3 text-sm tracking-wide-caps transition-[color,background-color,border-color,transform] duration-300 active:scale-[0.97] bg-transparent text-forest border border-forest hover:bg-forest hover:text-cream"
>
{t.account.ssoButton}
</a>
</>
) : null}
</div>
<form action={registerAction} className="space-y-4 md:pl-12"> <form action={registerAction} className="space-y-4 md:pl-12">
<h2 className="text-3xl text-forest">{t.account.register}</h2> <h2 className="text-3xl text-forest">{t.account.register}</h2>
@@ -80,34 +106,6 @@ export default async function KontoPage() {
</form> </form>
</div> </div>
<div className="mt-10 flex items-center gap-4 text-xs text-ink/70">
<span className="h-px flex-1 bg-ink/10" />
{t.account.ssoDivider}
<span className="h-px flex-1 bg-ink/10" />
</div>
<div className="mt-6">
{/*
Bewusst ein einfaches <a> statt LinkButton/next/link: der Klick
muss eine ECHTE Browser-Top-Level-Navigation ausloesen, die dem
302-Redirect dieser Route bis zu login.microsoftonline.com
folgt. Next.js' <Link> navigiert clientseitig per fetch() —
dieser Cross-Origin-Redirect-Sprung wuerde dann von der
connect-src-CSP in proxy.ts blockiert (die nur "self" u.ae.
erlaubt), waehrend eine echte Top-Level-Navigation von CSP
connect-src gar nicht erst betroffen ist.
Bewusst immer sichtbar, auch ohne konfiguriertes SSO (siehe
.env.example, ENTRA_*) — der Link fuehrt in dem Fall zu einer
klaren Fehlermeldung auf dieser Seite statt eines defekten
Buttons (src/app/api/auth/entra/login/route.ts prueft die
Konfiguration server-seitig, bevor irgendetwas passiert).
*/}
<a
href="/api/auth/entra/login"
className="inline-flex items-center justify-center gap-2 px-7 py-3 text-sm tracking-wide-caps transition-[color,background-color,border-color,transform] duration-300 active:scale-[0.97] bg-transparent text-forest border border-forest hover:bg-forest hover:text-cream"
>
{t.account.ssoButton}
</a>
</div>
</Container> </Container>
</div> </div>
); );
@@ -121,7 +119,7 @@ export default async function KontoPage() {
return ( return (
<div className="py-20"> <div className="py-20">
<Container className="max-w-2xl"> <Container className="max-w-2xl">
<SectionHeading eyebrow={t.account.eyebrow} title={t.account.greeting(customer.firstName)} /> <SectionHeading title={t.account.greeting(customer.firstName)} />
{!customer.emailVerifiedAt ? ( {!customer.emailVerifiedAt ? (
+1 -1
View File
@@ -17,7 +17,7 @@ export default async function PasswortVergessenPage(props: PageProps<"/konto/pas
<Container className="max-w-lg"> <Container className="max-w-lg">
<FormPanel> <FormPanel>
<BackLink href="/konto" label={t.passwortVergessenPage.backLabel} /> <BackLink href="/konto" label={t.passwortVergessenPage.backLabel} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.passwortVergessenPage.title} /> <SectionHeading title={t.passwortVergessenPage.title} />
{sent ? ( {sent ? (
@@ -20,7 +20,7 @@ export default async function PasswortZuruecksetzenPage(
<Container className="max-w-lg"> <Container className="max-w-lg">
<FormPanel> <FormPanel>
<BackLink href="/konto" label={t.passwortZuruecksetzenPage.backLabel} /> <BackLink href="/konto" label={t.passwortZuruecksetzenPage.backLabel} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.passwortZuruecksetzenPage.title} /> <SectionHeading title={t.passwortZuruecksetzenPage.title} />
{!token ? ( {!token ? (
+1 -1
View File
@@ -24,7 +24,7 @@ export default async function KontoWunschlistePage() {
<div className="py-20"> <div className="py-20">
<Container> <Container>
<BackLink href="/konto" label={t.ordersPage.backToAccount} /> <BackLink href="/konto" label={t.ordersPage.backToAccount} />
<SectionHeading eyebrow={t.account.eyebrow} title={t.wishlistPage.title} /> <SectionHeading title={t.wishlistPage.title} />
{products.length === 0 ? ( {products.length === 0 ? (
<AccountEmptyState <AccountEmptyState
-1
View File
@@ -23,7 +23,6 @@ export default async function ZweiFaktorCodePage() {
<FormPanel> <FormPanel>
<BackLink href="/konto" label={t.twoFactorCodePage.cancelLabel} /> <BackLink href="/konto" label={t.twoFactorCodePage.cancelLabel} />
<SectionHeading <SectionHeading
eyebrow={t.twoFactorCodePage.eyebrow}
title={t.twoFactorCodePage.title} title={t.twoFactorCodePage.title}
subtitle={t.twoFactorCodePage.subtitle} subtitle={t.twoFactorCodePage.subtitle}
/> />
@@ -22,7 +22,6 @@ export default async function ZweiFaktorBackupCodesPage() {
<Container className="max-w-lg"> <Container className="max-w-lg">
<FormPanel> <FormPanel>
<SectionHeading <SectionHeading
eyebrow={t.twoFactorBackupCodesPage.eyebrow}
title={t.twoFactorBackupCodesPage.title} title={t.twoFactorBackupCodesPage.title}
subtitle={t.twoFactorBackupCodesPage.subtitle} subtitle={t.twoFactorBackupCodesPage.subtitle}
/> />
@@ -28,7 +28,6 @@ export default async function ZweiFaktorEinrichtenPage() {
<FormPanel> <FormPanel>
<BackLink href="/konto/zwei-faktor" label={t.twoFactorSetupPage.backLabel} /> <BackLink href="/konto/zwei-faktor" label={t.twoFactorSetupPage.backLabel} />
<SectionHeading <SectionHeading
eyebrow={t.twoFactorSetupPage.eyebrow}
title={t.twoFactorSetupPage.title} title={t.twoFactorSetupPage.title}
subtitle={t.twoFactorSetupPage.subtitle} subtitle={t.twoFactorSetupPage.subtitle}
/> />
-1
View File
@@ -29,7 +29,6 @@ export default async function ZweiFaktorPage() {
<FormPanel> <FormPanel>
<BackLink href="/konto" label={t.twoFactorPage.backLabel} /> <BackLink href="/konto" label={t.twoFactorPage.backLabel} />
<SectionHeading <SectionHeading
eyebrow={t.twoFactorPage.eyebrow}
title={t.twoFactorPage.title} title={t.twoFactorPage.title}
subtitle={t.twoFactorPage.subtitle} subtitle={t.twoFactorPage.subtitle}
/> />
-1
View File
@@ -31,7 +31,6 @@ export default async function NewsletterBestaetigtPage(props: PageProps<"/newsle
<div className="py-24"> <div className="py-24">
<Container className="max-w-xl text-center"> <Container className="max-w-xl text-center">
<SectionHeading <SectionHeading
eyebrow={t.newsletterBestaetigtPage.eyebrow}
title={title} title={title}
align="center" align="center"
className="mx-auto" className="mx-auto"
-1
View File
@@ -50,7 +50,6 @@ export default async function NotFound() {
})} })}
</svg> </svg>
<SectionHeading <SectionHeading
eyebrow={t.notFoundPage.eyebrow}
title={t.notFoundPage.title} title={t.notFoundPage.title}
align="center" align="center"
className="mx-auto" className="mx-auto"
-1
View File
@@ -88,7 +88,6 @@ export default async function ShopPage(props: PageProps<"/shop">) {
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading
level="h1" level="h1"
eyebrow={t.shop.eyebrow}
title={t.shop.title} title={t.shop.title}
subtitle={t.shop.subtitle} subtitle={t.shop.subtitle}
/> />
-20
View File
@@ -41,31 +41,11 @@ export default async function UnternehmenPage() {
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading
level="h1" level="h1"
eyebrow={t.unternehmenPage.eyebrow}
title={t.unternehmenPage.title} title={t.unternehmenPage.title}
subtitle={t.unternehmenPage.subtitle} subtitle={t.unternehmenPage.subtitle}
/> />
</ScrollReveal> </ScrollReveal>
<div className="mt-14 grid sm:grid-cols-3 gap-6">
{t.unternehmenPage.packages.map((pkg, i) => (
<ScrollReveal key={pkg.key} delay={i * 0.08}>
<div className="group relative h-full overflow-hidden border border-ink/10 bg-background p-8 transition-[transform,border-color,box-shadow] duration-500 ease-[var(--ease-arrive)] hover:-translate-y-1 hover:border-honey/60 hover:shadow-[0_22px_40px_-24px_rgba(184,138,68,0.45)]">
{/* Grosses Sechseck als Wasserzeichen, das beim Hover einrueckt. */}
<svg
viewBox="0 0 24 26"
aria-hidden="true"
className="absolute -right-6 -top-6 h-28 w-28 text-honey/15 transition-transform duration-700 ease-[var(--ease-arrive)] group-hover:-translate-x-2 group-hover:translate-y-2 group-hover:rotate-12"
>
<polygon points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7" fill="currentColor" />
</svg>
<h3 className="relative text-2xl text-forest">{pkg.title}</h3>
<p className="relative mt-3 text-sm text-ink/70 leading-relaxed">{pkg.text}</p>
</div>
</ScrollReveal>
))}
</div>
{/* Geteilt: die Ueberschrift laeuft neben dem langen Formular mit. */} {/* Geteilt: die Ueberschrift laeuft neben dem langen Formular mit. */}
<div className="mt-24 grid gap-10 lg:grid-cols-12 lg:gap-16"> <div className="mt-24 grid gap-10 lg:grid-cols-12 lg:gap-16">
<ScrollReveal className="lg:col-span-4"> <ScrollReveal className="lg:col-span-4">
+18 -4
View File
@@ -56,6 +56,9 @@ export function WarenkorbContent({
const crossSellProducts = availableProducts const crossSellProducts = availableProducts
.filter((product) => !cartProductIds.has(product.id)) .filter((product) => !cartProductIds.has(product.id))
.slice(0, CROSS_SELL_LIMIT); .slice(0, CROSS_SELL_LIMIT);
// Fuer den leeren Warenkorb: Honig zuerst, sonst was gerade verfuegbar ist.
const honeyFirst = availableProducts.filter((product) => product.category === "HONIG");
const emptyCartSuggestions = (honeyFirst.length > 0 ? honeyFirst : availableProducts).slice(0, 2);
// Siehe checkout-form.tsx: ohne diese Weiche flackert kurz die // Siehe checkout-form.tsx: ohne diese Weiche flackert kurz die
// "leer"-Meldung auf, bevor der Cart-Store aus localStorage nachliest. // "leer"-Meldung auf, bevor der Cart-Store aus localStorage nachliest.
@@ -66,15 +69,26 @@ export function WarenkorbContent({
return ( return (
<div className="py-16"> <div className="py-16">
<Container> <Container>
<SectionHeading eyebrow={t.cart.title} title={t.cartPage.title} /> <SectionHeading title={t.cartPage.title} />
{items.length === 0 ? ( {items.length === 0 ? (
<div className="mt-14"> // Leerer Warenkorb ist kein Endpunkt: daneben stehen die aktuell
<p className="text-ink/70">{t.cart.empty}</p> // verfuegbaren Honige, statt nur eines Satzes und eines Knopfs.
<LinkButton href="/shop" className="mt-6"> <div className="mt-14 grid gap-12 lg:grid-cols-12 lg:gap-16">
<div className="lg:col-span-4">
<p className="font-serif text-2xl leading-snug text-forest">{t.cart.empty}</p>
<LinkButton href="/shop" variant="secondary" className="mt-6">
{t.cartPage.goToShop} {t.cartPage.goToShop}
</LinkButton> </LinkButton>
</div> </div>
{emptyCartSuggestions.length > 0 ? (
<div className="grid grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2 lg:col-span-8">
{emptyCartSuggestions.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
</div>
) : null}
</div>
) : ( ) : (
<div className="mt-14 grid lg:grid-cols-3 gap-14"> <div className="mt-14 grid lg:grid-cols-3 gap-14">
<div className="lg:col-span-2 divide-y divide-ink/10 border-y border-ink/10"> <div className="lg:col-span-2 divide-y divide-ink/10 border-y border-ink/10">
+19 -12
View File
@@ -19,36 +19,43 @@ export default async function WissenPage() {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
return ( return (
<div className="py-20"> <div className="py-20">
<Container className="max-w-3xl"> <Container className="max-w-5xl">
<ScrollReveal> <ScrollReveal>
<SectionHeading level="h1" eyebrow={t.wissenListPage.eyebrow} title={t.wissenListPage.title} /> <SectionHeading level="h1" title={t.wissenListPage.title} />
</ScrollReveal> </ScrollReveal>
<div className="mt-14 divide-y divide-ink/10 border-y border-ink/10"> {/* Inhaltsverzeichnis statt zehn gleicher Bloecke mit je einem
"Weiterlesen": die ganze Zeile ist der Link, Titel und Kurztext
stehen nebeneinander, der Pfeil zeigt die Richtung. */}
<ol className="mt-14 divide-y divide-ink/10 border-y border-ink/10">
{wissenSlugs.map((slug, i) => { {wissenSlugs.map((slug, i) => {
const article = t.wissenArticles[slug]; const article = t.wissenArticles[slug];
return ( return (
<ScrollReveal key={slug} delay={(i % 5) * 0.06}> <li key={slug}>
<Link href={`/wissen/${slug}`} className="group block py-9"> <ScrollReveal delay={(i % 5) * 0.05}>
<h2 className="link-draw inline pb-0.5 text-3xl leading-tight text-forest">{article.title}</h2> <Link
<p className="mt-3 max-w-[60ch] text-ink/70 leading-relaxed">{article.excerpt}</p> href={`/wissen/${slug}`}
<span className="mt-4 inline-flex items-center gap-2 text-sm text-honey-dark tracking-wide-caps"> className="group grid gap-3 py-8 md:grid-cols-12 md:items-baseline md:gap-8"
{t.wissenListPage.readMore} >
<h2 className="text-2xl leading-tight text-forest text-balance md:col-span-5 md:text-3xl">
<span className="link-draw pb-0.5">{article.title}</span>
</h2>
<p className="max-w-[60ch] text-ink/70 leading-relaxed md:col-span-6">{article.excerpt}</p>
<svg <svg
aria-hidden="true" aria-hidden="true"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="h-3.5 w-3.5 transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1" className="hidden h-5 w-5 justify-self-end text-honey-dark transition-transform duration-300 ease-[var(--ease-arrive)] group-hover:translate-x-1 motion-reduce:transition-none md:col-span-1 md:block"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
strokeWidth="1.6" strokeWidth="1.6"
> >
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" /> <path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</span>
</Link> </Link>
</ScrollReveal> </ScrollReveal>
</li>
); );
})} })}
</div> </ol>
</Container> </Container>
</div> </div>
); );
+22 -11
View File
@@ -86,19 +86,30 @@ export function BusinessInquiryForm({ turnstileSiteKey }: { turnstileSiteKey?: s
<TextField name="desiredDeliveryDate" label={t.businessInquiryForm.deliveryDateLabel} type="date" /> <TextField name="desiredDeliveryDate" label={t.businessInquiryForm.deliveryDateLabel} type="date" />
</div> </div>
<label className="block text-sm"> {/* Die Pakete als waehlbare Karten direkt im Formular — frueher standen
<span className="text-ink/70">{t.businessInquiryForm.packageLabel}</span> sie als Deko-Kacheln ueber dem Formular und darin noch einmal als
<select Auswahlliste. Gleicher Feldname und dieselben Werte wie zuvor. */}
name="package" <fieldset className="text-sm">
className="mt-1 w-full border border-ink/20 bg-background px-4 py-3" <legend className="text-ink/70">{t.businessInquiryForm.packageLabel}</legend>
<div className="mt-2 grid gap-3 sm:grid-cols-3">
{t.unternehmenPage.packages.map((p, i) => (
<label
key={p.key}
className="group relative flex cursor-pointer flex-col border border-ink/20 bg-background p-4 transition-colors duration-300 hover:border-ink/40 has-[:checked]:border-forest has-[:checked]:bg-forest/[0.04] has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-honey-dark"
> >
{t.unternehmenPage.packages.map((p) => ( <input type="radio" name="package" value={p.key} defaultChecked={i === 0} className="sr-only" />
<option key={p.key} value={p.key}> <span className="flex items-center justify-between gap-3">
{p.title} — {p.text} <span className="font-serif text-xl text-forest">{p.title}</span>
</option> <span
))} aria-hidden="true"
</select> className="h-3.5 w-3.5 flex-shrink-0 rounded-full border border-ink/30 transition-[border-width,border-color] duration-200 group-has-[:checked]:border-[4.5px] group-has-[:checked]:border-forest"
/>
</span>
<span className="mt-2 text-ink/70 leading-relaxed">{p.text}</span>
</label> </label>
))}
</div>
</fieldset>
<label className="block text-sm"> <label className="block text-sm">
<span className="text-ink/70">{t.businessInquiryForm.messageLabel}</span> <span className="text-ink/70">{t.businessInquiryForm.messageLabel}</span>
+40 -39
View File
@@ -1,4 +1,4 @@
import { Container, SectionHeading, LinkButton, FramedMedia } from "@/components/ui"; import { Container, SectionHeading, LinkButton } from "@/components/ui";
import { HoneycombPattern, HexMark } from "@/components/honeycomb"; import { HoneycombPattern, HexMark } from "@/components/honeycomb";
import { ScrollReveal } from "@/components/scroll-reveal"; import { ScrollReveal } from "@/components/scroll-reveal";
import { ContentImage } from "@/components/content-image"; import { ContentImage } from "@/components/content-image";
@@ -17,17 +17,25 @@ export async function CurrentHarvest({
reviewSummaries?: Map<string, { average: number | null; count: number }>; reviewSummaries?: Map<string, { average: number | null; count: number }>;
}) { }) {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
// Ausserhalb der Erntezeit sind es oft nur ein, zwei Glaeser. Dann steht die
// Ueberschrift als eigene Spalte daneben, statt ueber einem halb leeren
// Dreierraster.
const isFew = products.length <= 2;
return ( return (
<section className="py-24"> <section className="py-24">
<Container> <Container className={isFew ? "grid gap-12 lg:grid-cols-12 lg:gap-16" : ""}>
<ScrollReveal> <ScrollReveal className={isFew ? "lg:col-span-4" : ""}>
<SectionHeading <div className={isFew ? "lg:sticky lg:top-32" : ""}>
eyebrow={t.home.currentHarvest.eyebrow} <SectionHeading title={t.home.currentHarvest.title} subtitle={t.home.currentHarvest.subtitle} />
title={t.home.currentHarvest.title} </div>
subtitle={t.home.currentHarvest.subtitle}
/>
</ScrollReveal> </ScrollReveal>
<div className="mt-14 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-14"> <div
className={
isFew
? "grid grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2 lg:col-span-8"
: "mt-14 grid grid-cols-1 gap-x-8 gap-y-14 sm:grid-cols-2 lg:grid-cols-3"
}
>
{products.map((product, index) => ( {products.map((product, index) => (
<ProductCard <ProductCard
key={product.id} key={product.id}
@@ -56,25 +64,23 @@ export async function OriginStory() {
); );
const facts = parseListSimple(content["home.originStory.facts"]); const facts = parseListSimple(content["home.originStory.facts"]);
return ( return (
<section className="py-28 bg-cream/60"> // Randlos geteilt: das Foto fuellt die linke Haelfte bis an den
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center"> // Fensterrand, der Text steht daneben — kein Rahmen, keine Karte.
<ScrollReveal> <section className="grid bg-cream/60 md:grid-cols-2">
<FramedMedia> <ScrollReveal className="h-full">
<ContentImage <ContentImage
src={content["home.originStory.image"] || null} src={content["home.originStory.image"] || null}
alt="" alt=""
tone="forest" tone="forest"
className="aspect-[4/5] w-full" rounded={false}
className="h-full min-h-[24rem] w-full md:min-h-[40rem]"
label={t.home.originStory.eyebrow} label={t.home.originStory.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw" sizes="(min-width: 768px) 50vw, 100vw"
/> />
</FramedMedia>
</ScrollReveal> </ScrollReveal>
<ScrollReveal delay={0.1}> <div className="flex items-center px-6 py-20 sm:px-10 lg:px-20 xl:px-28">
<SectionHeading <ScrollReveal delay={0.1} className="max-w-xl">
eyebrow={t.home.originStory.eyebrow} <SectionHeading title={t.home.originStory.title} />
title={t.home.originStory.title}
/>
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.originStory.paragraph1"]}</p> <p className="mt-6 text-ink/75 leading-relaxed">{content["home.originStory.paragraph1"]}</p>
<p className="mt-4 text-ink/75 leading-relaxed">{content["home.originStory.paragraph2"]}</p> <p className="mt-4 text-ink/75 leading-relaxed">{content["home.originStory.paragraph2"]}</p>
<ul className="mt-8 grid gap-2.5 text-sm text-ink/75 sm:grid-cols-2"> <ul className="mt-8 grid gap-2.5 text-sm text-ink/75 sm:grid-cols-2">
@@ -85,11 +91,11 @@ export async function OriginStory() {
</li> </li>
))} ))}
</ul> </ul>
<LinkButton href="/herkunft" variant="secondary" className="mt-8"> <LinkButton href="/herkunft" variant="secondary" className="mt-10">
{t.home.originStory.cta} {t.home.originStory.cta}
</LinkButton> </LinkButton>
</ScrollReveal> </ScrollReveal>
</Container> </div>
</section> </section>
); );
} }
@@ -100,27 +106,22 @@ export async function Philosophy() {
const content = await getContentValues(["home.philosophy.items"], locale); const content = await getContentValues(["home.philosophy.items"], locale);
const items = parseListTitled(content["home.philosophy.items"]); const items = parseListTitled(content["home.philosophy.items"]);
return ( return (
<section className="py-24"> <section className="py-24 md:py-28">
<Container> <Container>
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading title={t.home.philosophy.title} />
eyebrow={t.home.philosophy.eyebrow}
title={t.home.philosophy.title}
align="center"
className="mx-auto"
/>
</ScrollReveal> </ScrollReveal>
<div className="mt-16 grid sm:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-14"> {/* Typografische Reihe statt Icon-Karten: vier Grundsaetze, getrennt
durch Linien, die Titel tragen das Gewicht. */}
<div className="mt-14 grid border-t border-ink/15 sm:grid-cols-2 lg:grid-cols-4">
{items.map((item, i) => ( {items.map((item, i) => (
<ScrollReveal key={item.title} delay={i * 0.08}> <ScrollReveal
<div className={`border-t border-ink/15 pt-6 ${i % 2 === 1 ? "lg:mt-10" : ""}`}> key={item.title}
{/* Wabe statt "01"-Nummer: die vier Punkte sind gleichrangig, keine Reihenfolge. */} delay={i * 0.06}
<HexMark filled={i % 2 === 0} className="h-5 w-5 text-honey" /> className="border-b border-ink/15 py-8 sm:pr-8 lg:border-b-0 lg:border-l lg:px-8 lg:first:border-l-0 lg:first:pl-0"
<h3 className="mt-4 text-2xl text-forest">{item.title}</h3> >
<p className="mt-3 text-sm text-ink/70 leading-relaxed"> <h3 className="font-serif text-3xl leading-tight text-forest text-balance">{item.title}</h3>
{item.text} <p className="mt-3 max-w-[34ch] text-sm leading-relaxed text-ink/70">{item.text}</p>
</p>
</div>
</ScrollReveal> </ScrollReveal>
))} ))}
</div> </div>
+33 -37
View File
@@ -1,4 +1,4 @@
import { Container, SectionHeading, LinkButton, FramedMedia } from "@/components/ui"; import { Container, SectionHeading, LinkButton } from "@/components/ui";
import { ProcessPath } from "@/components/process-path"; import { ProcessPath } from "@/components/process-path";
import { HoneycombPattern, HexMark } from "@/components/honeycomb"; import { HoneycombPattern, HexMark } from "@/components/honeycomb";
import { ScrollReveal } from "@/components/scroll-reveal"; import { ScrollReveal } from "@/components/scroll-reveal";
@@ -19,7 +19,6 @@ export async function ProcessTimeline() {
<Container> <Container>
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading
eyebrow={t.home.processTimeline.eyebrow}
title={t.home.processTimeline.title} title={t.home.processTimeline.title}
align="center" align="center"
className="mx-auto" className="mx-auto"
@@ -44,12 +43,24 @@ export async function RegionalSection() {
); );
return ( return (
<section className="py-24"> <section className="py-24">
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center"> <Container>
<ScrollReveal className="order-2 md:order-1"> {/* Breites Bild, der Text legt sich als Tafel darueber — Tiefe statt
<SectionHeading eines weiteren Bild-neben-Text-Wechsels. */}
eyebrow={t.home.regionalSection.eyebrow} <ScrollReveal>
title={t.home.regionalSection.title} <ContentImage
src={content["home.regionalSection.image"] || null}
alt=""
tone="forest"
className="aspect-[4/3] w-full sm:aspect-[16/9]"
label={t.home.regionalSection.eyebrow}
sizes="(min-width: 1280px) 1200px, 100vw"
/> />
</ScrollReveal>
<ScrollReveal
delay={0.1}
className="relative mx-4 -mt-16 max-w-2xl bg-background p-8 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)] sm:mx-10 sm:p-10 lg:-mt-40 lg:ml-16 lg:p-12"
>
<SectionHeading title={t.home.regionalSection.title} />
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.regionalSection.paragraph1"]}</p> <p className="mt-6 text-ink/75 leading-relaxed">{content["home.regionalSection.paragraph1"]}</p>
<p className="mt-4 text-ink/75 leading-relaxed">{content["home.regionalSection.paragraph2"]}</p> <p className="mt-4 text-ink/75 leading-relaxed">{content["home.regionalSection.paragraph2"]}</p>
<div className="mt-8 flex flex-wrap gap-4"> <div className="mt-8 flex flex-wrap gap-4">
@@ -61,18 +72,6 @@ export async function RegionalSection() {
</LinkButton> </LinkButton>
</div> </div>
</ScrollReveal> </ScrollReveal>
<ScrollReveal className="order-1 md:order-2" delay={0.1}>
<FramedMedia side="left">
<ContentImage
src={content["home.regionalSection.image"] || null}
alt=""
tone="forest"
className="aspect-[4/5] w-full"
label={t.home.regionalSection.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw"
/>
</FramedMedia>
</ScrollReveal>
</Container> </Container>
</section> </section>
); );
@@ -90,7 +89,7 @@ export async function Sustainability() {
<Container className="grid gap-12 lg:grid-cols-12 lg:gap-16"> <Container className="grid gap-12 lg:grid-cols-12 lg:gap-16">
<ScrollReveal className="lg:col-span-5"> <ScrollReveal className="lg:col-span-5">
<div className="lg:sticky lg:top-32"> <div className="lg:sticky lg:top-32">
<SectionHeading eyebrow={t.home.sustainability.eyebrow} title={t.home.sustainability.title} /> <SectionHeading title={t.home.sustainability.title} />
</div> </div>
</ScrollReveal> </ScrollReveal>
<ul className="divide-y divide-ink/10 border-y border-ink/10 lg:col-span-7"> <ul className="divide-y divide-ink/10 border-y border-ink/10 lg:col-span-7">
@@ -122,28 +121,26 @@ export async function BusinessTeaser() {
); );
return ( return (
<section className="py-24"> <section className="py-24">
<Container className="grid md:grid-cols-2 gap-16 lg:gap-24 items-center"> <Container>
<ScrollReveal> {/* Kompakte Tafel statt eines dritten grossen Bild/Text-Blocks: ein
<FramedMedia> Angebot fuer eine kleinere Zielgruppe, entsprechend zurueckhaltend. */}
<ScrollReveal className="grid overflow-hidden border border-ink/10 bg-background md:grid-cols-5">
<div className="flex flex-col justify-center p-8 sm:p-12 md:col-span-3 lg:p-16">
<SectionHeading title={t.home.businessTeaser.title} />
<p className="mt-6 max-w-[55ch] text-ink/75 leading-relaxed">{content["home.businessTeaser.paragraph"]}</p>
<LinkButton href="/unternehmen" className="mt-8 self-start">
{t.home.businessTeaser.cta}
</LinkButton>
</div>
<ContentImage <ContentImage
src={content["home.businessTeaser.image"] || null} src={content["home.businessTeaser.image"] || null}
alt="" alt=""
tone="honey" tone="honey"
className="aspect-[4/5] w-full" rounded={false}
className="aspect-[4/3] w-full md:col-span-2 md:aspect-auto md:h-full md:min-h-[22rem]"
label={t.home.businessTeaser.eyebrow} label={t.home.businessTeaser.eyebrow}
sizes="(min-width: 768px) 50vw, 100vw" sizes="(min-width: 768px) 40vw, 100vw"
/> />
</FramedMedia>
</ScrollReveal>
<ScrollReveal delay={0.1}>
<SectionHeading
eyebrow={t.home.businessTeaser.eyebrow}
title={t.home.businessTeaser.title}
/>
<p className="mt-6 text-ink/75 leading-relaxed">{content["home.businessTeaser.paragraph"]}</p>
<LinkButton href="/unternehmen" className="mt-8">
{t.home.businessTeaser.cta}
</LinkButton>
</ScrollReveal> </ScrollReveal>
</Container> </Container>
</section> </section>
@@ -174,7 +171,6 @@ export async function Reviews() {
<Container className="relative"> <Container className="relative">
<ScrollReveal> <ScrollReveal>
<SectionHeading <SectionHeading
eyebrow={t.home.reviews.eyebrow}
title={t.home.reviews.title} title={t.home.reviews.title}
align="center" align="center"
className="mx-auto" className="mx-auto"
-1
View File
@@ -60,7 +60,6 @@ export function NewsletterSignup({ turnstileSiteKey }: { turnstileSiteKey?: stri
<HoneycombPattern className="text-honey/25" size={24} /> <HoneycombPattern className="text-honey/25" size={24} />
<div className="relative mx-auto max-w-2xl"> <div className="relative mx-auto max-w-2xl">
<SectionHeading <SectionHeading
eyebrow={t.home.newsletter.eyebrow}
title={t.home.newsletter.title} title={t.home.newsletter.title}
align="center" align="center"
className="mx-auto" className="mx-auto"
+49
View File
@@ -0,0 +1,49 @@
import Link from "next/link";
import { Container } from "@/components/ui";
export interface PageOutroLink {
href: string;
label: string;
text: string;
}
/**
* Abschluss einer Inhaltsseite (Imkerei, Herkunft, Ernten): zwei naechste
* Wege als grosse Serif-Links, getrennt durch eine Linie. Ohne ihn endeten
* diese Seiten mit dem letzten Absatz direkt vor der Fusszeile — eine
* Sackgasse, gerade fuer Besuch, der ueber die Suche mitten einsteigt.
*/
export function PageOutro({ links, ariaLabel }: { links: PageOutroLink[]; ariaLabel: string }) {
return (
<nav aria-label={ariaLabel} className="mt-24 border-t border-ink/10">
<Container className="grid md:grid-cols-2">
{links.map((link, i) => (
<Link
key={link.href}
href={link.href}
className={`group flex items-end justify-between gap-6 py-12 md:py-16 ${
i === 0 ? "md:pr-12" : "border-t border-ink/10 md:border-l md:border-t-0 md:pl-12"
}`}
>
<span>
<span className="block font-serif text-4xl leading-tight text-forest md:text-5xl">
{link.label}
</span>
<span className="mt-3 block max-w-[40ch] text-sm leading-relaxed text-ink/65">{link.text}</span>
</span>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
className="mb-2 h-6 w-6 flex-shrink-0 text-honey-dark transition-transform duration-500 ease-[var(--ease-arrive)] group-hover:translate-x-1.5 motion-reduce:transition-none"
>
<path d="M4 12h15M13 6l6 6-6 6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Link>
))}
</Container>
</nav>
);
}
+26 -29
View File
@@ -1,7 +1,20 @@
import { HoneycombPattern } from "@/components/honeycomb";
import { Logomark } from "@/components/logomark";
const SURFACES = {
forest: "bg-forest text-cream",
cream: "bg-cream text-forest",
honey: "bg-honey-pale/60 text-forest",
} as const;
/** /**
* Abstrakter, hochwertiger Bild-Platzhalter (Farbverlauf + feine Linientextur) * Platzhalter, solange fuer ein Produkt oder einen Abschnitt kein Foto
* fuer Bereiche, in denen echte Produkt-/Naturfotografie noch fehlt. Bewusst * gepflegt ist. Gestaltet wie das Glas-Etikett: ruhige Flaeche, das
* ohne Comic-Motive oder Wabenmuster-Kitsch, passend zur Markenpalette. * gezeichnete Waben-Zeichen und darunter der Name in gesperrten
* Serif-Versalien — statt eines generischen Farbverlaufs mit Linientextur.
*
* In kleinen Groessen (Warenkorb, Galerie-Vorschau) bleibt nur das Zeichen;
* der Name erscheint erst ab 9rem Breite (Container Query).
*/ */
export function PlaceholderImage({ export function PlaceholderImage({
className = "", className = "",
@@ -9,39 +22,23 @@ export function PlaceholderImage({
label, label,
}: { }: {
className?: string; className?: string;
tone?: "forest" | "cream" | "honey"; tone?: keyof typeof SURFACES;
label?: string; label?: string;
}) { }) {
const gradients: Record<string, string> = {
forest: "from-[#1a271f] via-[#24352b] to-[#3a4d3f]",
cream: "from-[#efe8d8] via-[#f4efe5] to-[#e2d9c4]",
honey: "from-[#8a6832] via-[#b88a44] to-[#c9a468]",
};
return ( return (
<div <div className={`@container relative overflow-hidden ${SURFACES[tone]} ${className}`}>
className={`relative overflow-hidden bg-gradient-to-br ${gradients[tone]} ${className}`} <HoneycombPattern className="opacity-[0.07]" size={20} fade="radial" />
> <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 p-4 text-center">
<svg <Logomark
className="absolute inset-0 w-full h-full opacity-[0.15]" className="aspect-square h-auto w-[26%] max-w-20 opacity-70"
xmlns="http://www.w3.org/2000/svg" variant={tone === "forest" ? "inverted" : "default"}
> />
<defs>
<pattern id="lines" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M0 40L40 0" stroke="currentColor" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#lines)" />
</svg>
{label ? ( {label ? (
<span <span className="hidden max-w-[80%] font-serif text-base uppercase leading-snug tracking-[0.2em] opacity-60 @min-[9rem]:line-clamp-2">
className={`absolute bottom-4 left-4 right-4 truncate text-[11px] tracking-wide-caps ${
tone === "cream" ? "text-forest/50" : "text-cream/50"
}`}
>
{label} {label}
</span> </span>
) : null} ) : null}
</div> </div>
</div>
); );
} }
+1 -1
View File
@@ -63,7 +63,7 @@ export function ProductGallery({ images, productName, description }: ProductGall
return ( return (
<div> <div>
<div <div
className="relative aspect-[4/3] overflow-hidden rounded-lg touch-pan-y" className="relative aspect-[4/3] overflow-hidden rounded-lg touch-pan-y md:aspect-[4/5]"
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd} onTouchEnd={handleTouchEnd}
> >
+1 -1
View File
@@ -84,7 +84,7 @@ function Logo({ t, companyName, logoSrc }: { t: Dictionary; companyName: string;
<span className="text-2xl tracking-[0.2em] text-forest font-serif"> <span className="text-2xl tracking-[0.2em] text-forest font-serif">
{companyName} {companyName}
</span> </span>
<span className="hidden xl:block text-[10px] tracking-[0.3em] uppercase text-honey-dark mt-1 whitespace-nowrap"> <span className="hidden 2xl:block text-[10px] tracking-[0.3em] uppercase text-honey-dark mt-1 whitespace-nowrap">
{t.footer.tagline} {t.footer.tagline}
</span> </span>
</span> </span>
+5 -14
View File
@@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import type { ComponentPropsWithoutRef, ReactNode } from "react"; import type { ComponentPropsWithoutRef, ReactNode } from "react";
import { HexMark, HoneycombPattern } from "@/components/honeycomb"; import { HoneycombPattern } from "@/components/honeycomb";
export function Container({ export function Container({
id, id,
@@ -82,7 +82,6 @@ export function Button({
} }
export function SectionHeading({ export function SectionHeading({
eyebrow,
title, title,
subtitle, subtitle,
align = "left", align = "left",
@@ -90,14 +89,13 @@ export function SectionHeading({
level = "h2", level = "h2",
tone = "default", tone = "default",
}: { }: {
eyebrow?: string;
title: string; title: string;
subtitle?: string; subtitle?: string;
align?: "left" | "center"; align?: "left" | "center";
className?: string; className?: string;
/** "h1" fuer Seiten, auf denen diese Komponente die einzige/primaere Ueberschrift ist (SEO-Heading-Hierarchie). Default h2, da die meisten Verwendungen einzelne Abschnitte innerhalb einer Seite mit eigenem h1 sind. */ /** "h1" fuer Seiten, auf denen diese Komponente die einzige/primaere Ueberschrift ist (SEO-Heading-Hierarchie). Default h2, da die meisten Verwendungen einzelne Abschnitte innerhalb einer Seite mit eigenem h1 sind. */
level?: "h1" | "h2"; level?: "h1" | "h2";
/** "inverted" fuer Abschnitte auf dunklem Grund (z.B. bg-forest) — steuert die Eyebrow-Farbe fuer ausreichenden Kontrast. */ /** "inverted" fuer Abschnitte auf dunklem Grund (z.B. bg-forest) — helle Ueberschrift und Untertitel. */
tone?: "default" | "inverted"; tone?: "default" | "inverted";
}) { }) {
const Heading = level; const Heading = level;
@@ -105,16 +103,9 @@ export function SectionHeading({
<div <div
className={`${align === "center" ? "text-center mx-auto" : "text-left"} max-w-2xl ${className}`} className={`${align === "center" ? "text-center mx-auto" : "text-left"} max-w-2xl ${className}`}
> >
{eyebrow ? ( {/* Bewusst ohne Kicker-Zeile ("• STIMMEN") ueber der Ueberschrift: auf
<p jeder Ueberschrift wiederholt wirkte das wie eine Vorlage. Die
className={`flex items-center gap-2 tracking-wide-caps text-xs mb-4 ${ Ueberschrift traegt den Abschnitt allein. */}
align === "center" ? "justify-center" : ""
} ${tone === "inverted" ? "text-honey-light" : "text-honey-dark"}`}
>
<HexMark className="h-2.5 w-2.5 flex-shrink-0" />
{eyebrow}
</p>
) : null}
<Heading <Heading
className={`text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] ${ className={`text-4xl md:text-5xl leading-[1.05] tracking-[-0.01em] ${
tone === "inverted" ? "text-cream" : "text-forest" tone === "inverted" ? "text-cream" : "text-forest"
+6 -19
View File
@@ -14,6 +14,12 @@ export const de: Dictionary = {
retry: "Erneut versuchen", retry: "Erneut versuchen",
}, },
}, },
pageOutro: {
ariaLabel: "Weiterlesen",
shopText: "Honig, Bienenwachskerzen und Propolis aus eigener Imkerei — solange die Charge reicht.",
harvestsText: "Jede Ernte mit Datum, Standort und Charge, zurückverfolgbar bis ins Glas.",
originText: "Wo unsere Bienenvölker stehen und was die Landschaft um Altenmünster prägt.",
},
nav: { nav: {
shop: "Shop", shop: "Shop",
harvests: "Unsere Ernten", harvests: "Unsere Ernten",
@@ -156,14 +162,12 @@ export const de: Dictionary = {
reviewNotice: "", reviewNotice: "",
}, },
notFoundPage: { notFoundPage: {
eyebrow: "404",
title: "Diese Seite gibt es nicht, zumindest nicht mehr.", title: "Diese Seite gibt es nicht, zumindest nicht mehr.",
text: "Der Link ist vielleicht veraltet, oder die Seite wurde verschoben. Schau dich im Shop um oder geh zurück zur Startseite.", text: "Der Link ist vielleicht veraltet, oder die Seite wurde verschoben. Schau dich im Shop um oder geh zurück zur Startseite.",
backToShop: "Zum Shop", backToShop: "Zum Shop",
backToHome: "Zur Startseite", backToHome: "Zur Startseite",
}, },
siteErrorPage: { siteErrorPage: {
eyebrow: "Fehler",
title: "Da ist etwas schiefgelaufen.", title: "Da ist etwas schiefgelaufen.",
text: "Das war ein technischer Fehler auf unserer Seite, nicht deine Schuld. Versuch es noch einmal, oder komm später wieder vorbei.", text: "Das war ein technischer Fehler auf unserer Seite, nicht deine Schuld. Versuch es noch einmal, oder komm später wieder vorbei.",
retry: "Erneut versuchen", retry: "Erneut versuchen",
@@ -478,7 +482,6 @@ export const de: Dictionary = {
wissenListPage: { wissenListPage: {
eyebrow: "Honigwissen", eyebrow: "Honigwissen",
title: "Wissenswertes rund um Honig", title: "Wissenswertes rund um Honig",
readMore: "Weiterlesen",
}, },
wissenArticlePage: { wissenArticlePage: {
backLabel: "Zurück zum Honigwissen", backLabel: "Zurück zum Honigwissen",
@@ -585,7 +588,6 @@ export const de: Dictionary = {
"Nach Absprache kannst du deine Bestellung direkt bei uns in Altenmünster abholen. Wähle dazu im Checkout einfach „Abholung“ aus. Wir melden uns anschließend mit einem Abholtermin.", "Nach Absprache kannst du deine Bestellung direkt bei uns in Altenmünster abholen. Wähle dazu im Checkout einfach „Abholung“ aus. Wir melden uns anschließend mit einem Abholtermin.",
}, },
twoFactorPage: { twoFactorPage: {
eyebrow: "Sicherheit",
title: "Zwei-Faktor-Authentifizierung", title: "Zwei-Faktor-Authentifizierung",
subtitle: "Schützt dein Konto zusätzlich zum Passwort mit einem Code aus einer Authenticator-App.", subtitle: "Schützt dein Konto zusätzlich zum Passwort mit einem Code aus einer Authenticator-App.",
backLabel: "Zurück zur Übersicht", backLabel: "Zurück zur Übersicht",
@@ -601,7 +603,6 @@ export const de: Dictionary = {
emptyDescription: "Auf jeder Produktseite kannst du sie über „Zur Wunschliste hinzufügen“ speichern.", emptyDescription: "Auf jeder Produktseite kannst du sie über „Zur Wunschliste hinzufügen“ speichern.",
}, },
twoFactorCodePage: { twoFactorCodePage: {
eyebrow: "Kundenkonto",
title: "Anmeldung bestätigen", title: "Anmeldung bestätigen",
subtitle: "Gib den 6-stelligen Code aus deiner Authenticator-App ein, oder verwende einen deiner Backup-Codes.", subtitle: "Gib den 6-stelligen Code aus deiner Authenticator-App ein, oder verwende einen deiner Backup-Codes.",
cancelLabel: "Anmeldung abbrechen", cancelLabel: "Anmeldung abbrechen",
@@ -609,7 +610,6 @@ export const de: Dictionary = {
confirm: "Bestätigen", confirm: "Bestätigen",
}, },
twoFactorBackupCodesPage: { twoFactorBackupCodesPage: {
eyebrow: "Sicherheit",
title: "Deine Backup-Codes", title: "Deine Backup-Codes",
subtitle: subtitle:
"Jeder Code funktioniert einmalig, falls du keinen Zugriff auf deine Authenticator-App hast. Speichere sie an einem sicheren Ort. Sie werden dir nur jetzt angezeigt.", "Jeder Code funktioniert einmalig, falls du keinen Zugriff auf deine Authenticator-App hast. Speichere sie an einem sicheren Ort. Sie werden dir nur jetzt angezeigt.",
@@ -617,7 +617,6 @@ export const de: Dictionary = {
done: "Fertig", done: "Fertig",
}, },
twoFactorSetupPage: { twoFactorSetupPage: {
eyebrow: "Sicherheit",
title: "Zwei-Faktor-Authentifizierung einrichten", title: "Zwei-Faktor-Authentifizierung einrichten",
subtitle: subtitle:
"Scanne den QR-Code mit einer Authenticator-App (z. B. Google Authenticator, Authy) und bestätige mit dem angezeigten Code.", "Scanne den QR-Code mit einer Authenticator-App (z. B. Google Authenticator, Authy) und bestätige mit dem angezeigten Code.",
@@ -636,7 +635,6 @@ export const de: Dictionary = {
submit: "Anfrage senden", submit: "Anfrage senden",
}, },
erntenPage: { erntenPage: {
eyebrow: "Ernten",
title: "Kein Jahr schmeckt gleich.", title: "Kein Jahr schmeckt gleich.",
batchCount: (count) => `${count} Charge${count === 1 ? "" : "n"}`, batchCount: (count) => `${count} Charge${count === 1 ? "" : "n"}`,
emptyText: "Noch keine Ernten hinterlegt.", emptyText: "Noch keine Ernten hinterlegt.",
@@ -657,7 +655,6 @@ export const de: Dictionary = {
fromThisBatch: "Aus dieser Charge", fromThisBatch: "Aus dieser Charge",
}, },
newsletterBestaetigtPage: { newsletterBestaetigtPage: {
eyebrow: "Erntepost",
confirmedTitle: "Anmeldung bestätigt.", confirmedTitle: "Anmeldung bestätigt.",
invalidTitle: "Link nicht mehr gültig.", invalidTitle: "Link nicht mehr gültig.",
unsubscribedTitle: "Abmeldung erfolgreich.", unsubscribedTitle: "Abmeldung erfolgreich.",
@@ -902,7 +899,6 @@ export const de: Dictionary = {
save: "Speichern", save: "Speichern",
}, },
trackOrderPage: { trackOrderPage: {
eyebrow: "Bestellung verfolgen",
title: "Wo ist meine Bestellung?", title: "Wo ist meine Bestellung?",
intro: "Gib deine Bestellnummer und die bei der Bestellung verwendete E-Mail-Adresse ein, um den aktuellen Status zu sehen, auch ohne Kundenkonto.", intro: "Gib deine Bestellnummer und die bei der Bestellung verwendete E-Mail-Adresse ein, um den aktuellen Status zu sehen, auch ohne Kundenkonto.",
orderNumberLabel: "Bestellnummer", orderNumberLabel: "Bestellnummer",
@@ -1402,7 +1398,6 @@ export const de: Dictionary = {
save: "Adresse speichern", save: "Adresse speichern",
}, },
account: { account: {
eyebrow: "Kundenkonto",
loginRegisterTitle: "Anmelden oder Konto erstellen", loginRegisterTitle: "Anmelden oder Konto erstellen",
login: "Anmelden", login: "Anmelden",
register: "Konto erstellen", register: "Konto erstellen",
@@ -1491,7 +1486,6 @@ export const de: Dictionary = {
backToCart: "Zurück zum Warenkorb", backToCart: "Zurück zum Warenkorb",
}, },
checkoutCancelPage: { checkoutCancelPage: {
eyebrow: "Abgebrochen",
title: "Deine Bestellung wurde nicht abgeschlossen.", title: "Deine Bestellung wurde nicht abgeschlossen.",
text: "Kein Problem, dein Warenkorb ist noch gefüllt. Du kannst den Bestellvorgang jederzeit erneut starten.", text: "Kein Problem, dein Warenkorb ist noch gefüllt. Du kannst den Bestellvorgang jederzeit erneut starten.",
released: "Die für dich zurückgelegte Ware steht wieder im Shop zur Verfügung.", released: "Die für dich zurückgelegte Ware steht wieder im Shop zur Verfügung.",
@@ -1644,7 +1638,6 @@ export const de: Dictionary = {
starLabel: (value) => `${value} ${value === 1 ? "Stern" : "Sterne"}`, starLabel: (value) => `${value} ${value === 1 ? "Stern" : "Sterne"}`,
}, },
shop: { shop: {
eyebrow: "Shop",
title: "Unser Sortiment", title: "Unser Sortiment",
subtitle: "Honig, Bienenwachskerzen und Propolis aus eigener Imkerei, solange der jeweilige Bestand reicht.", subtitle: "Honig, Bienenwachskerzen und Propolis aus eigener Imkerei, solange der jeweilige Bestand reicht.",
allCategories: "Alle", allCategories: "Alle",
@@ -1670,7 +1663,6 @@ export const de: Dictionary = {
footnote: "Geerntet · abgefüllt · versendet aus eigener Imkerei", footnote: "Geerntet · abgefüllt · versendet aus eigener Imkerei",
}, },
currentHarvest: { currentHarvest: {
eyebrow: "Aktuell verfügbar",
title: "Die aktuelle Ernte", title: "Die aktuelle Ernte",
subtitle: "Kleine, dokumentierte Chargen, solange der Vorrat reicht.", subtitle: "Kleine, dokumentierte Chargen, solange der Vorrat reicht.",
}, },
@@ -1685,7 +1677,6 @@ export const de: Dictionary = {
cta: "Mehr über die Herkunft", cta: "Mehr über die Herkunft",
}, },
philosophy: { philosophy: {
eyebrow: "Philosophie",
title: "Die Wabenhain-Philosophie", title: "Die Wabenhain-Philosophie",
items: [ items: [
{ title: "Eigene Ernte", text: "Unser Honig stammt aus unseren eigenen Bienenvölkern." }, { title: "Eigene Ernte", text: "Unser Honig stammt aus unseren eigenen Bienenvölkern." },
@@ -1704,7 +1695,6 @@ export const de: Dictionary = {
cta: "Ernte entdecken", cta: "Ernte entdecken",
}, },
processTimeline: { processTimeline: {
eyebrow: "Vom Bienenvolk zum Glas",
title: "Sechs Schritte bis zu deinem Honig", title: "Sechs Schritte bis zu deinem Honig",
steps: [ steps: [
{ number: "01", title: "Blüte", text: "Die Bienen sammeln Nektar aus der Umgebung." }, { number: "01", title: "Blüte", text: "Die Bienen sammeln Nektar aus der Umgebung." },
@@ -1725,7 +1715,6 @@ export const de: Dictionary = {
ctaPickup: "Honig vor Ort abholen", ctaPickup: "Honig vor Ort abholen",
}, },
sustainability: { sustainability: {
eyebrow: "Verantwortung",
title: "Verantwortung beginnt im Kleinen.", title: "Verantwortung beginnt im Kleinen.",
items: [ items: [
{ title: "Wiederverwendbare Gläser", text: "Saubere Wabenhain-Gläser können bei uns zurückgegeben werden." }, { title: "Wiederverwendbare Gläser", text: "Saubere Wabenhain-Gläser können bei uns zurückgegeben werden." },
@@ -1741,7 +1730,6 @@ export const de: Dictionary = {
cta: "Firmengeschenke anfragen", cta: "Firmengeschenke anfragen",
}, },
reviews: { reviews: {
eyebrow: "Stimmen",
title: "Was unsere Kund:innen sagen", title: "Was unsere Kund:innen sagen",
googleReviewsCount: (rating, count) => `${rating} ★ · ${count} Google-Rezensionen`, googleReviewsCount: (rating, count) => `${rating} ★ · ${count} Google-Rezensionen`,
seeAllOnGoogle: "Alle Bewertungen auf Google ansehen →", seeAllOnGoogle: "Alle Bewertungen auf Google ansehen →",
@@ -1759,7 +1747,6 @@ export const de: Dictionary = {
], ],
}, },
newsletter: { newsletter: {
eyebrow: "Erntepost",
title: "Erfahre als Erstes von der nächsten Ernte.", title: "Erfahre als Erstes von der nächsten Ernte.",
description: description:
"Neue Ernten gibt es nur einige Male im Jahr. Mit unserer Erntepost erfährst du, sobald neuer Honig verfügbar ist.", "Neue Ernten gibt es nur einige Male im Jahr. Mit unserer Erntepost erfährst du, sobald neuer Honig verfügbar ist.",
+6 -19
View File
@@ -14,6 +14,12 @@ export const en: Dictionary = {
retry: "Try again", retry: "Try again",
}, },
}, },
pageOutro: {
ariaLabel: "Keep reading",
shopText: "Honey, beeswax candles and propolis from our own apiary — while each batch lasts.",
harvestsText: "Every harvest with date, location and batch, traceable down to the jar.",
originText: "Where our colonies live and what shapes the landscape around Altenmünster.",
},
nav: { nav: {
shop: "Shop", shop: "Shop",
harvests: "Our Harvests", harvests: "Our Harvests",
@@ -156,14 +162,12 @@ export const en: Dictionary = {
reviewNotice: "", reviewNotice: "",
}, },
notFoundPage: { notFoundPage: {
eyebrow: "404",
title: "This page doesn't exist, at least not anymore.", title: "This page doesn't exist, at least not anymore.",
text: "The link might be outdated, or the page has moved. Take a look around the shop, or head back to the homepage.", text: "The link might be outdated, or the page has moved. Take a look around the shop, or head back to the homepage.",
backToShop: "Go to shop", backToShop: "Go to shop",
backToHome: "Back to homepage", backToHome: "Back to homepage",
}, },
siteErrorPage: { siteErrorPage: {
eyebrow: "Error",
title: "Something went wrong.", title: "Something went wrong.",
text: "That was a technical error on our end, not something you did. Try again, or come back a bit later.", text: "That was a technical error on our end, not something you did. Try again, or come back a bit later.",
retry: "Try again", retry: "Try again",
@@ -466,7 +470,6 @@ export const en: Dictionary = {
wissenListPage: { wissenListPage: {
eyebrow: "Honey Knowledge", eyebrow: "Honey Knowledge",
title: "Good to know about honey", title: "Good to know about honey",
readMore: "Read more",
}, },
wissenArticlePage: { wissenArticlePage: {
backLabel: "Back to Honey Knowledge", backLabel: "Back to Honey Knowledge",
@@ -573,7 +576,6 @@ export const en: Dictionary = {
"By arrangement, you can pick up your order directly from us in Altenmünster. Just select “Pickup” at checkout. We'll then get in touch with a pickup time.", "By arrangement, you can pick up your order directly from us in Altenmünster. Just select “Pickup” at checkout. We'll then get in touch with a pickup time.",
}, },
twoFactorPage: { twoFactorPage: {
eyebrow: "Security",
title: "Two-Factor Authentication", title: "Two-Factor Authentication",
subtitle: "Protects your account beyond just a password, with a code from an authenticator app.", subtitle: "Protects your account beyond just a password, with a code from an authenticator app.",
backLabel: "Back to overview", backLabel: "Back to overview",
@@ -589,7 +591,6 @@ export const en: Dictionary = {
emptyDescription: "You can save products from any product page via “Add to wishlist.”", emptyDescription: "You can save products from any product page via “Add to wishlist.”",
}, },
twoFactorCodePage: { twoFactorCodePage: {
eyebrow: "Customer account",
title: "Confirm sign-in", title: "Confirm sign-in",
subtitle: "Enter the 6-digit code from your authenticator app, or use one of your backup codes.", subtitle: "Enter the 6-digit code from your authenticator app, or use one of your backup codes.",
cancelLabel: "Cancel sign-in", cancelLabel: "Cancel sign-in",
@@ -597,7 +598,6 @@ export const en: Dictionary = {
confirm: "Confirm", confirm: "Confirm",
}, },
twoFactorBackupCodesPage: { twoFactorBackupCodesPage: {
eyebrow: "Security",
title: "Your backup codes", title: "Your backup codes",
subtitle: subtitle:
"Each code works once, in case you don't have access to your authenticator app. Store them somewhere safe. They're only shown now.", "Each code works once, in case you don't have access to your authenticator app. Store them somewhere safe. They're only shown now.",
@@ -605,7 +605,6 @@ export const en: Dictionary = {
done: "Done", done: "Done",
}, },
twoFactorSetupPage: { twoFactorSetupPage: {
eyebrow: "Security",
title: "Set up two-factor authentication", title: "Set up two-factor authentication",
subtitle: subtitle:
"Scan the QR code with an authenticator app (e.g. Google Authenticator, Authy) and confirm with the code shown.", "Scan the QR code with an authenticator app (e.g. Google Authenticator, Authy) and confirm with the code shown.",
@@ -624,7 +623,6 @@ export const en: Dictionary = {
submit: "Send request", submit: "Send request",
}, },
erntenPage: { erntenPage: {
eyebrow: "Harvests",
title: "No two years taste alike.", title: "No two years taste alike.",
batchCount: (count) => `${count} batch${count === 1 ? "" : "es"}`, batchCount: (count) => `${count} batch${count === 1 ? "" : "es"}`,
emptyText: "No harvests recorded yet.", emptyText: "No harvests recorded yet.",
@@ -645,7 +643,6 @@ export const en: Dictionary = {
fromThisBatch: "From this batch", fromThisBatch: "From this batch",
}, },
newsletterBestaetigtPage: { newsletterBestaetigtPage: {
eyebrow: "Harvest post",
confirmedTitle: "Subscription confirmed.", confirmedTitle: "Subscription confirmed.",
invalidTitle: "Link no longer valid.", invalidTitle: "Link no longer valid.",
unsubscribedTitle: "Unsubscribed successfully.", unsubscribedTitle: "Unsubscribed successfully.",
@@ -890,7 +887,6 @@ export const en: Dictionary = {
save: "Save", save: "Save",
}, },
trackOrderPage: { trackOrderPage: {
eyebrow: "Track order",
title: "Where's my order?", title: "Where's my order?",
intro: "Enter your order number and the email address used at checkout to see the current status, no account needed.", intro: "Enter your order number and the email address used at checkout to see the current status, no account needed.",
orderNumberLabel: "Order number", orderNumberLabel: "Order number",
@@ -1388,7 +1384,6 @@ export const en: Dictionary = {
save: "Save address", save: "Save address",
}, },
account: { account: {
eyebrow: "Account",
loginRegisterTitle: "Sign in or create an account", loginRegisterTitle: "Sign in or create an account",
login: "Sign in", login: "Sign in",
register: "Create account", register: "Create account",
@@ -1477,7 +1472,6 @@ export const en: Dictionary = {
backToCart: "Back to cart", backToCart: "Back to cart",
}, },
checkoutCancelPage: { checkoutCancelPage: {
eyebrow: "Cancelled",
title: "Your order was not completed.", title: "Your order was not completed.",
text: "No problem, your cart is still full. You can restart the checkout process at any time.", text: "No problem, your cart is still full. You can restart the checkout process at any time.",
released: "The items reserved for you are available in the shop again.", released: "The items reserved for you are available in the shop again.",
@@ -1630,7 +1624,6 @@ export const en: Dictionary = {
starLabel: (value) => `${value} star${value === 1 ? "" : "s"}`, starLabel: (value) => `${value} star${value === 1 ? "" : "s"}`,
}, },
shop: { shop: {
eyebrow: "Shop",
title: "Our Range", title: "Our Range",
subtitle: "Honey, beeswax candles, and propolis from our own apiary, while stocks last.", subtitle: "Honey, beeswax candles, and propolis from our own apiary, while stocks last.",
allCategories: "All", allCategories: "All",
@@ -1656,7 +1649,6 @@ export const en: Dictionary = {
footnote: "Harvested · bottled · shipped from our own apiary", footnote: "Harvested · bottled · shipped from our own apiary",
}, },
currentHarvest: { currentHarvest: {
eyebrow: "Currently available",
title: "The current harvest", title: "The current harvest",
subtitle: "Small, documented batches, while stocks last.", subtitle: "Small, documented batches, while stocks last.",
}, },
@@ -1670,7 +1662,6 @@ export const en: Dictionary = {
cta: "More about our origin", cta: "More about our origin",
}, },
philosophy: { philosophy: {
eyebrow: "Philosophy",
title: "The Wabenhain Philosophy", title: "The Wabenhain Philosophy",
items: [ items: [
{ title: "Our own harvest", text: "Our honey comes from our own bee colonies." }, { title: "Our own harvest", text: "Our honey comes from our own bee colonies." },
@@ -1689,7 +1680,6 @@ export const en: Dictionary = {
cta: "Discover this harvest", cta: "Discover this harvest",
}, },
processTimeline: { processTimeline: {
eyebrow: "From bee colony to jar",
title: "Six steps to your honey", title: "Six steps to your honey",
steps: [ steps: [
{ number: "01", title: "Blossom", text: "The bees collect nectar from the surrounding area." }, { number: "01", title: "Blossom", text: "The bees collect nectar from the surrounding area." },
@@ -1710,7 +1700,6 @@ export const en: Dictionary = {
ctaPickup: "Pick up honey locally", ctaPickup: "Pick up honey locally",
}, },
sustainability: { sustainability: {
eyebrow: "Responsibility",
title: "Responsibility starts small.", title: "Responsibility starts small.",
items: [ items: [
{ title: "Reusable jars", text: "Clean Wabenhain jars can be returned to us." }, { title: "Reusable jars", text: "Clean Wabenhain jars can be returned to us." },
@@ -1726,7 +1715,6 @@ export const en: Dictionary = {
cta: "Request corporate gifts", cta: "Request corporate gifts",
}, },
reviews: { reviews: {
eyebrow: "Voices",
title: "What our customers say", title: "What our customers say",
googleReviewsCount: (rating, count) => `${rating} ★ · ${count} Google reviews`, googleReviewsCount: (rating, count) => `${rating} ★ · ${count} Google reviews`,
seeAllOnGoogle: "See all reviews on Google →", seeAllOnGoogle: "See all reviews on Google →",
@@ -1744,7 +1732,6 @@ export const en: Dictionary = {
], ],
}, },
newsletter: { newsletter: {
eyebrow: "Harvest mail",
title: "Be the first to know about the next harvest.", title: "Be the first to know about the next harvest.",
description: description:
"New harvests only happen a few times a year. Our harvest mail lets you know as soon as new honey is available.", "New harvests only happen a few times a year. Our harvest mail lets you know as soon as new honey is available.",
+6 -19
View File
@@ -52,6 +52,12 @@ export interface Dictionary {
retry: string; retry: string;
}; };
}; };
pageOutro: {
ariaLabel: string;
shopText: string;
harvestsText: string;
originText: string;
};
nav: { nav: {
shop: string; shop: string;
harvests: string; harvests: string;
@@ -178,14 +184,12 @@ export interface Dictionary {
reviewNotice: string; reviewNotice: string;
}; };
notFoundPage: { notFoundPage: {
eyebrow: string;
title: string; title: string;
text: string; text: string;
backToShop: string; backToShop: string;
backToHome: string; backToHome: string;
}; };
siteErrorPage: { siteErrorPage: {
eyebrow: string;
title: string; title: string;
text: string; text: string;
retry: string; retry: string;
@@ -251,7 +255,6 @@ export interface Dictionary {
wissenListPage: { wissenListPage: {
eyebrow: string; eyebrow: string;
title: string; title: string;
readMore: string;
}; };
wissenArticlePage: { wissenArticlePage: {
backLabel: string; backLabel: string;
@@ -266,7 +269,6 @@ export interface Dictionary {
pickupText: string; pickupText: string;
}; };
twoFactorPage: { twoFactorPage: {
eyebrow: string;
title: string; title: string;
subtitle: string; subtitle: string;
backLabel: string; backLabel: string;
@@ -282,7 +284,6 @@ export interface Dictionary {
emptyDescription: string; emptyDescription: string;
}; };
twoFactorCodePage: { twoFactorCodePage: {
eyebrow: string;
title: string; title: string;
subtitle: string; subtitle: string;
cancelLabel: string; cancelLabel: string;
@@ -290,14 +291,12 @@ export interface Dictionary {
confirm: string; confirm: string;
}; };
twoFactorBackupCodesPage: { twoFactorBackupCodesPage: {
eyebrow: string;
title: string; title: string;
subtitle: string; subtitle: string;
warning: string; warning: string;
done: string; done: string;
}; };
twoFactorSetupPage: { twoFactorSetupPage: {
eyebrow: string;
title: string; title: string;
subtitle: string; subtitle: string;
backLabel: string; backLabel: string;
@@ -315,7 +314,6 @@ export interface Dictionary {
submit: string; submit: string;
}; };
erntenPage: { erntenPage: {
eyebrow: string;
title: string; title: string;
batchCount: (count: number) => string; batchCount: (count: number) => string;
emptyText: string; emptyText: string;
@@ -336,7 +334,6 @@ export interface Dictionary {
fromThisBatch: string; fromThisBatch: string;
}; };
newsletterBestaetigtPage: { newsletterBestaetigtPage: {
eyebrow: string;
confirmedTitle: string; confirmedTitle: string;
invalidTitle: string; invalidTitle: string;
unsubscribedTitle: string; unsubscribedTitle: string;
@@ -579,7 +576,6 @@ export interface Dictionary {
save: string; save: string;
}; };
trackOrderPage: { trackOrderPage: {
eyebrow: string;
title: string; title: string;
intro: string; intro: string;
orderNumberLabel: string; orderNumberLabel: string;
@@ -1056,7 +1052,6 @@ export interface Dictionary {
save: string; save: string;
}; };
account: { account: {
eyebrow: string;
loginRegisterTitle: string; loginRegisterTitle: string;
login: string; login: string;
register: string; register: string;
@@ -1139,7 +1134,6 @@ export interface Dictionary {
backToCart: string; backToCart: string;
}; };
checkoutCancelPage: { checkoutCancelPage: {
eyebrow: string;
title: string; title: string;
text: string; text: string;
released: string; released: string;
@@ -1269,7 +1263,6 @@ export interface Dictionary {
starLabel: (value: number) => string; starLabel: (value: number) => string;
}; };
shop: { shop: {
eyebrow: string;
title: string; title: string;
subtitle: string; subtitle: string;
allCategories: string; allCategories: string;
@@ -1295,7 +1288,6 @@ export interface Dictionary {
footnote: string; footnote: string;
}; };
currentHarvest: { currentHarvest: {
eyebrow: string;
title: string; title: string;
subtitle: string; subtitle: string;
}; };
@@ -1308,7 +1300,6 @@ export interface Dictionary {
cta: string; cta: string;
}; };
philosophy: { philosophy: {
eyebrow: string;
title: string; title: string;
items: TitledText[]; items: TitledText[];
}; };
@@ -1322,7 +1313,6 @@ export interface Dictionary {
cta: string; cta: string;
}; };
processTimeline: { processTimeline: {
eyebrow: string;
title: string; title: string;
steps: { number: string; title: string; text: string }[]; steps: { number: string; title: string; text: string }[];
}; };
@@ -1335,7 +1325,6 @@ export interface Dictionary {
ctaPickup: string; ctaPickup: string;
}; };
sustainability: { sustainability: {
eyebrow: string;
title: string; title: string;
items: TitledText[]; items: TitledText[];
}; };
@@ -1346,7 +1335,6 @@ export interface Dictionary {
cta: string; cta: string;
}; };
reviews: { reviews: {
eyebrow: string;
title: string; title: string;
googleReviewsCount: (rating: string, count: number) => string; googleReviewsCount: (rating: string, count: number) => string;
seeAllOnGoogle: string; seeAllOnGoogle: string;
@@ -1354,7 +1342,6 @@ export interface Dictionary {
testimonials: { quote: string; author: string }[]; testimonials: { quote: string; author: string }[];
}; };
newsletter: { newsletter: {
eyebrow: string;
title: string; title: string;
description: string; description: string;
successMessage: string; successMessage: string;