Compare commits
8
Commits
a4581d3467
...
fdb6456207
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdb6456207 | ||
|
|
0fb176add8 | ||
|
|
774bc9cf00 | ||
|
|
b588975317 | ||
|
|
908afebacb | ||
|
|
7c305a3fce | ||
|
|
7e467a9ddf | ||
|
|
1fa8ca246a |
No files matched your search
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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 ? (
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,14 +69,25 @@ 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">
|
||||||
{t.cartPage.goToShop}
|
<div className="lg:col-span-4">
|
||||||
</LinkButton>
|
<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}
|
||||||
|
</LinkButton>
|
||||||
|
</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>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-14 grid lg:grid-cols-3 gap-14">
|
<div className="mt-14 grid lg:grid-cols-3 gap-14">
|
||||||
|
|||||||
+21
-14
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) => (
|
{t.unternehmenPage.packages.map((p, i) => (
|
||||||
<option key={p.key} value={p.key}>
|
<label
|
||||||
{p.title} — {p.text}
|
key={p.key}
|
||||||
</option>
|
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"
|
||||||
|
>
|
||||||
|
<input type="radio" name="package" value={p.key} defaultChecked={i === 0} className="sr-only" />
|
||||||
|
<span className="flex items-center justify-between gap-3">
|
||||||
|
<span className="font-serif text-xl text-forest">{p.title}</span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
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>
|
||||||
))}
|
))}
|
||||||
</select>
|
</div>
|
||||||
</label>
|
</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>
|
||||||
|
|||||||
@@ -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}
|
||||||
label={t.home.originStory.eyebrow}
|
className="h-full min-h-[24rem] w-full md:min-h-[40rem]"
|
||||||
sizes="(min-width: 768px) 50vw, 100vw"
|
label={t.home.originStory.eyebrow}
|
||||||
/>
|
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>
|
||||||
|
|||||||
@@ -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. */}
|
||||||
<ContentImage
|
<ScrollReveal className="grid overflow-hidden border border-ink/10 bg-background md:grid-cols-5">
|
||||||
src={content["home.businessTeaser.image"] || null}
|
<div className="flex flex-col justify-center p-8 sm:p-12 md:col-span-3 lg:p-16">
|
||||||
alt=""
|
<SectionHeading title={t.home.businessTeaser.title} />
|
||||||
tone="honey"
|
<p className="mt-6 max-w-[55ch] text-ink/75 leading-relaxed">{content["home.businessTeaser.paragraph"]}</p>
|
||||||
className="aspect-[4/5] w-full"
|
<LinkButton href="/unternehmen" className="mt-8 self-start">
|
||||||
label={t.home.businessTeaser.eyebrow}
|
{t.home.businessTeaser.cta}
|
||||||
sizes="(min-width: 768px) 50vw, 100vw"
|
</LinkButton>
|
||||||
/>
|
</div>
|
||||||
</FramedMedia>
|
<ContentImage
|
||||||
</ScrollReveal>
|
src={content["home.businessTeaser.image"] || null}
|
||||||
<ScrollReveal delay={0.1}>
|
alt=""
|
||||||
<SectionHeading
|
tone="honey"
|
||||||
eyebrow={t.home.businessTeaser.eyebrow}
|
rounded={false}
|
||||||
title={t.home.businessTeaser.title}
|
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}
|
||||||
|
sizes="(min-width: 768px) 40vw, 100vw"
|
||||||
/>
|
/>
|
||||||
<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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
{label ? (
|
||||||
<pattern id="lines" width="40" height="40" patternUnits="userSpaceOnUse">
|
<span className="hidden max-w-[80%] font-serif text-base uppercase leading-snug tracking-[0.2em] opacity-60 @min-[9rem]:line-clamp-2">
|
||||||
<path d="M0 40L40 0" stroke="currentColor" strokeWidth="0.5" />
|
{label}
|
||||||
</pattern>
|
</span>
|
||||||
</defs>
|
) : null}
|
||||||
<rect width="100%" height="100%" fill="url(#lines)" />
|
</div>
|
||||||
</svg>
|
|
||||||
{label ? (
|
|
||||||
<span
|
|
||||||
className={`absolute bottom-4 left-4 right-4 truncate text-[11px] tracking-wide-caps ${
|
|
||||||
tone === "cream" ? "text-forest/50" : "text-cream/50"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user