@@ -150,7 +151,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo
) : null}
-
{formatPrice(order.totalCents, order.currency)}
+
{formatPrice(order.totalCents, order.currency, locale)}
{order.invoice ? (
{t.trackOrderPage.viewInvoice}
diff --git a/src/app/charge/[code]/page.tsx b/src/app/charge/[code]/page.tsx
index b08c84c..b7fe17f 100644
--- a/src/app/charge/[code]/page.tsx
+++ b/src/app/charge/[code]/page.tsx
@@ -27,15 +27,16 @@ export async function generateMetadata(
* in dem jemand mit dem Glas in der Hand die Herkunftsangaben nachprueft.
*/
export default async function BatchPage(props: PageProps<"/charge/[code]">) {
- const t = getDictionary(await getLocale());
+ const locale = await getLocale();
+ const t = getDictionary(locale);
const { code } = await props.params;
const batch = await getBatchByCode(code);
if (!batch) notFound();
const steps = [
- { label: t.chargePage.stepHarvest, value: formatDate(batch.harvest.harvestDate) },
- { label: t.chargePage.stepFilling, value: formatDate(batch.fillDate) },
- { label: t.chargePage.stepBestBefore, value: formatDate(batch.bestBeforeDate) },
+ { label: t.chargePage.stepHarvest, value: formatDate(batch.harvest.harvestDate, locale) },
+ { label: t.chargePage.stepFilling, value: formatDate(batch.fillDate, locale) },
+ { label: t.chargePage.stepBestBefore, value: formatDate(batch.bestBeforeDate, locale) },
];
return (
diff --git a/src/app/checkout/checkout-form.tsx b/src/app/checkout/checkout-form.tsx
index 9945d98..fd676b9 100644
--- a/src/app/checkout/checkout-form.tsx
+++ b/src/app/checkout/checkout-form.tsx
@@ -31,7 +31,7 @@ export function CheckoutForm({
tiers: ShippingTierLike[];
bulkDiscountTiers: BulkDiscountTierLike[];
}) {
- const { t } = useI18n();
+ const { t, locale } = useI18n();
const items = useCartStore((s) => s.items);
const subtotal = cartTotalCents(items);
const totalQuantity = cartItemCount(items);
@@ -267,14 +267,14 @@ export function CheckoutForm({
{item.quantity} × {item.name}
-
{formatPrice(item.priceCents * item.quantity)}
+
{formatPrice(item.priceCents * item.quantity, "EUR", locale)}
))}
{t.cart.subtotal}
- {formatPrice(subtotal)}
+ {formatPrice(subtotal, "EUR", locale)}
@@ -282,20 +282,20 @@ export function CheckoutForm({
? t.checkoutPage.shippingWithCount(totalQuantity)
: t.checkoutPage.shippingPickup}
- {shipping === 0 ? t.cartPage.free : formatPrice(shipping)}
+ {shipping === 0 ? t.cartPage.free : formatPrice(shipping, "EUR", locale)}
{activeBulkDiscount ? (
{t.cartPage.discountLabel} ({activeBulkDiscount.percent}%)
- -{formatPrice(bulkDiscountCents)}
+ -{formatPrice(bulkDiscountCents, "EUR", locale)}
) : null}
{t.cartPage.total}
- {formatPrice(total)}
+ {formatPrice(total, "EUR", locale)}
diff --git a/src/app/ernten/[year]/[type]/page.tsx b/src/app/ernten/[year]/[type]/page.tsx
index d094ccf..0315bc4 100644
--- a/src/app/ernten/[year]/[type]/page.tsx
+++ b/src/app/ernten/[year]/[type]/page.tsx
@@ -30,7 +30,8 @@ export async function generateMetadata(
export default async function HarvestDetailPage(
props: PageProps<"/ernten/[year]/[type]">
) {
- const t = getDictionary(await getLocale());
+ const locale = await getLocale();
+ const t = getDictionary(locale);
const { year, type } = await props.params;
// Next.js liefert dynamische Segmente hier unkodiert (z.B. "Fr%C3%BChtracht"
// statt "Frühtracht") — deshalb wird explizit dekodiert, statt sich auf
@@ -53,7 +54,7 @@ export default async function HarvestDetailPage(
/>
- {harvest.year} · {formatDate(harvest.harvestDate)}
+ {harvest.year} · {formatDate(harvest.harvestDate, locale)}
{harvest.type}
{harvest.description}
diff --git a/src/app/ernten/page.tsx b/src/app/ernten/page.tsx
index beeed30..ab8baa9 100644
--- a/src/app/ernten/page.tsx
+++ b/src/app/ernten/page.tsx
@@ -17,7 +17,8 @@ export const metadata: Metadata = {
export const dynamic = "force-dynamic";
export default async function ErntenPage() {
- const t = getDictionary(await getLocale());
+ const locale = await getLocale();
+ const t = getDictionary(locale);
const harvests = await getHarvests();
const byYear = harvests.reduce
>((acc, h) => {
acc[h.year] = [...(acc[h.year] ?? []), h];
@@ -46,7 +47,7 @@ export default async function ErntenPage() {
alt={harvest.type}
tone="forest"
className="aspect-[4/3] w-full transition-transform duration-500 group-hover:scale-[1.02]"
- label={formatMonthYear(harvest.harvestDate)}
+ label={formatMonthYear(harvest.harvestDate, locale)}
/>
{harvest.type}
{t.erntenPage.batchCount(harvest.batches.length)}
diff --git a/src/app/honig/[slug]/page.tsx b/src/app/honig/[slug]/page.tsx
index a5b56fb..34efe2d 100644
--- a/src/app/honig/[slug]/page.tsx
+++ b/src/app/honig/[slug]/page.tsx
@@ -45,7 +45,8 @@ export async function generateMetadata(
export default async function ProductDetailPage(
props: PageProps<"/honig/[slug]">
) {
- const t = getDictionary(await getLocale());
+ const locale = await getLocale();
+ const t = getDictionary(locale);
const { slug } = await props.params;
const { error, message, notifyError, notifyMessage } = await props.searchParams;
const product = await getProductBySlug(slug);
@@ -167,18 +168,20 @@ export default async function ProductDetailPage(
- {formatPrice(product.priceCents, product.currency)}
+ {formatPrice(product.priceCents, product.currency, locale)}
{product.weightGrams} g
- {formatPricePerKg(product.pricePerKgCents, product.currency)}
+ {formatPricePerKg(product.pricePerKgCents, product.currency, locale)}
- {t.productPage.shippingHint(baseShippingCents === 0 ? t.cartPage.free : formatPrice(baseShippingCents))}
+ {t.productPage.shippingHint(
+ baseShippingCents === 0 ? t.cartPage.free : formatPrice(baseShippingCents, "EUR", locale)
+ )}
{isHoney ? (
@@ -270,7 +273,10 @@ export default async function ProductDetailPage(
label={t.productPage.detailLabels.origin}
value={`${product.harvest.location} — ${product.harvest.type} ${product.harvest.year}`}
/>
-
+
@@ -311,7 +317,7 @@ export default async function ProductDetailPage(
{t.productPage.starsOutOf5(review.rating)}
{review.comment ? {review.comment}
: null}
- {review.customer.firstName} · {formatDate(review.createdAt)}
+ {review.customer.firstName} · {formatDate(review.createdAt, locale)}
))}
diff --git a/src/app/konto/bestellungen/page.tsx b/src/app/konto/bestellungen/page.tsx
index bf8f35a..ef108af 100644
--- a/src/app/konto/bestellungen/page.tsx
+++ b/src/app/konto/bestellungen/page.tsx
@@ -26,7 +26,8 @@ const RETURN_ELIGIBLE_STATUSES = [
];
export default async function KontoBestellungenPage(props: PageProps<"/konto/bestellungen">) {
- const t = getDictionary(await getLocale());
+ const locale = await getLocale();
+ const t = getDictionary(locale);
const statusLabels = t.orderStatusBadge;
const returnStatusLabels = t.ordersPage.returnStatusLabels;
const { error, message } = await props.searchParams;
@@ -65,19 +66,19 @@ export default async function KontoBestellungenPage(props: PageProps<"/konto/bes
{order.orderNumber}
-
{formatDate(order.createdAt)}
+
{formatDate(order.createdAt, locale)}
{order.fulfillmentMethod === "VERSAND"
? order.shipment?.shippedAt
- ? t.ordersPage.shippedOn(formatDate(order.shipment.shippedAt))
+ ? t.ordersPage.shippedOn(formatDate(order.shipment.shippedAt, locale))
: t.ordersPage.shippingPending
: order.pickup?.pickedUpAt
- ? t.ordersPage.pickedUpOn(formatDate(order.pickup.pickedUpAt))
+ ? t.ordersPage.pickedUpOn(formatDate(order.pickup.pickedUpAt, locale))
: order.pickup?.readyAt
- ? t.ordersPage.readySince(formatDate(order.pickup.readyAt))
+ ? t.ordersPage.readySince(formatDate(order.pickup.readyAt, locale))
: t.ordersPage.pickupPending}
@@ -138,7 +139,7 @@ export default async function KontoBestellungenPage(props: PageProps<"/konto/bes
) : null}
-
{formatPrice(order.totalCents, order.currency)}
+
{formatPrice(order.totalCents, order.currency, locale)}
{order.invoice ? (
0
? tiers.map((tier) =>
tier.maxQuantity === null
- ? t.versandZahlungPage.tierMinimum(tier.minQuantity, formatPrice(tier.priceCents))
- : t.versandZahlungPage.tierRange(tier.minQuantity, tier.maxQuantity, formatPrice(tier.priceCents))
+ ? t.versandZahlungPage.tierMinimum(tier.minQuantity, formatPrice(tier.priceCents, "EUR", locale))
+ : t.versandZahlungPage.tierRange(
+ tier.minQuantity,
+ tier.maxQuantity,
+ formatPrice(tier.priceCents, "EUR", locale)
+ )
)
: [t.versandZahlungPage.tierFallback];
diff --git a/src/app/warenkorb/warenkorb-content.tsx b/src/app/warenkorb/warenkorb-content.tsx
index c5b6139..a499fec 100644
--- a/src/app/warenkorb/warenkorb-content.tsx
+++ b/src/app/warenkorb/warenkorb-content.tsx
@@ -25,7 +25,7 @@ export function WarenkorbContent({
availableProducts: ProductWithRelations[];
bulkDiscountTiers: BulkDiscountTierLike[];
}) {
- const { t } = useI18n();
+ const { t, locale } = useI18n();
const items = useCartStore((s) => s.items);
const setQuantity = useCartStore((s) => s.setQuantity);
const removeItem = useCartStore((s) => s.removeItem);
@@ -96,7 +96,7 @@ export function WarenkorbContent({
max={itemStock ?? 10}
/>
- {formatPrice(item.priceCents * item.quantity)}
+ {formatPrice(item.priceCents * item.quantity, "EUR", locale)}
diff --git a/src/components/content-image.tsx b/src/components/content-image.tsx
index 2ec96d9..d35b876 100644
--- a/src/components/content-image.tsx
+++ b/src/components/content-image.tsx
@@ -10,6 +10,10 @@ interface ContentImageProps {
tone?: "forest" | "cream" | "honey";
label?: string;
sizes?: string;
+ /** Fuer das wahrscheinlichste LCP-Bild einer Seite (z.B. Startseiten-Hero)
+ * true setzen — laedt das Bild mit hoher Prioritaet ohne Lazy-Loading,
+ * statt es erst beim Scannen weiter unten im Dokument zu entdecken. */
+ priority?: boolean;
}
/**
@@ -29,6 +33,7 @@ export function ContentImage({
tone = "forest",
label,
sizes = "100vw",
+ priority = false,
}: ContentImageProps) {
if (!src) {
return ;
@@ -41,7 +46,7 @@ export function ContentImage({
const descriptiveAlt = label ? `${alt} – ${label}` : alt;
return (
-
+
);
}
diff --git a/src/components/hero.tsx b/src/components/hero.tsx
index eea2d53..5ca61c9 100644
--- a/src/components/hero.tsx
+++ b/src/components/hero.tsx
@@ -51,6 +51,7 @@ export function Hero({
tone="forest"
className="h-[78vh] min-h-[520px] w-full"
label="Bienenstand im Morgenlicht · Altenmünster"
+ priority
/>
diff --git a/src/components/home-sections-a.tsx b/src/components/home-sections-a.tsx
index f5a00a2..64bae69 100644
--- a/src/components/home-sections-a.tsx
+++ b/src/components/home-sections-a.tsx
@@ -128,7 +128,8 @@ type HoneySpotlightProduct = ProductWithRelations & {
};
export async function ProductSpotlight({ product }: { product: HoneySpotlightProduct }) {
- const t = getDictionary(await getLocale());
+ const locale = await getLocale();
+ const t = getDictionary(locale);
return (
@@ -162,7 +163,7 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
- {t.home.productSpotlight.harvest}
- - {formatDate(product.harvest.harvestDate)}
+ - {formatDate(product.harvest.harvestDate, locale)}
- {t.home.productSpotlight.batch}
@@ -174,7 +175,7 @@ export async function ProductSpotlight({ product }: { product: HoneySpotlightPro
- {t.home.productSpotlight.price}
- - {formatPrice(product.priceCents, product.currency)}
+ - {formatPrice(product.priceCents, product.currency, locale)}
diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx
index 4ba5f5b..71a63ef 100644
--- a/src/components/product-card.tsx
+++ b/src/components/product-card.tsx
@@ -173,7 +173,7 @@ export function ProductCard({
/** Optional — nur die Shop-Uebersicht laedt Bewertungen gebuendelt fuer das ganze Raster. */
reviewSummary?: { average: number | null; count: number };
}) {
- const { t } = useI18n();
+ const { t, locale } = useI18n();
const stock = product.inventory?.quantity ?? 0;
return (
@@ -229,11 +229,11 @@ export function ProductCard({
{product.weightGrams} g
- {formatPrice(product.priceCents, product.currency)}
+ {formatPrice(product.priceCents, product.currency, locale)}
- {formatPricePerKg(product.pricePerKgCents, product.currency)}
+ {formatPricePerKg(product.pricePerKgCents, product.currency, locale)}
{stock > 0 ? (
diff --git a/src/components/product-gallery.tsx b/src/components/product-gallery.tsx
index 078437a..b7ce985 100644
--- a/src/components/product-gallery.tsx
+++ b/src/components/product-gallery.tsx
@@ -1,6 +1,7 @@
"use client";
import { useRef, useState, type TouchEvent } from "react";
+import Image from "next/image";
import { PlaceholderImage } from "@/components/placeholder-image";
// Mindest-Wischstrecke in Pixeln, ab der ein Touch-Wisch als Bildwechsel
@@ -62,13 +63,25 @@ export function ProductGallery({ images, productName, description }: ProductGall
return (
{isImageSlide(active) ? (
- // eslint-disable-next-line @next/next/no-img-element -- externe Bildquelle noch unbekannt, next/image braucht dafuer eine feste remotePatterns-Konfiguration.
-

+ // Hochgeladene Produktbilder liegen immer same-origin unter
+ // /uploads/... (siehe uploads.ts/isValidUploadPath) — next/image
+ // braucht dafuer KEINEN remotePatterns-Eintrag (nur externe
+ // Bildquellen brauchen den), siehe auch content-image.tsx. Als
+ // wahrscheinlichstes LCP-Element der Produktseite mit "priority"
+ // geladen, statt erst beim Scannen des Bild-Tags entdeckt zu werden.
+
) : (
)}
@@ -82,13 +95,12 @@ export function ProductGallery({ images, productName, description }: ProductGall
onClick={() => setActiveIndex(index)}
aria-label={`${isImageSlide(slide) ? `Produktbild ${index + 1}` : slide.label} – Bild ${index + 1} von ${slides.length} anzeigen`}
aria-current={index === activeIndex}
- className={`aspect-square overflow-hidden border-2 transition-colors ${
+ className={`relative aspect-square overflow-hidden border-2 transition-colors ${
index === activeIndex ? "border-forest" : "border-transparent hover:border-ink/20"
}`}
>
{isImageSlide(slide) ? (
- // eslint-disable-next-line @next/next/no-img-element -- s.o.
-

+
) : (
)}
diff --git a/src/components/site-chrome.tsx b/src/components/site-chrome.tsx
index d3ad2d4..5e4f948 100644
--- a/src/components/site-chrome.tsx
+++ b/src/components/site-chrome.tsx
@@ -393,7 +393,7 @@ export function Header({ companyName }: { companyName: string }) {
}
export function CartDrawer() {
- const { t } = useI18n();
+ const { t, locale } = useI18n();
const isOpen = useCartStore((s) => s.isOpen);
const closeCart = useCartStore((s) => s.closeCart);
const items = useCartStore((s) => s.items);
@@ -499,7 +499,7 @@ export function CartDrawer() {
- {formatPrice(item.priceCents * item.quantity)}
+ {formatPrice(item.priceCents * item.quantity, "EUR", locale)}
);
@@ -512,7 +512,7 @@ export function CartDrawer() {
{t.cart.subtotal}
- {formatPrice(total)}
+ {formatPrice(total, "EUR", locale)}
{t.cart.checkout}
diff --git a/src/components/sticky-add-to-cart-bar.tsx b/src/components/sticky-add-to-cart-bar.tsx
index c1cab2e..7ff966d 100644
--- a/src/components/sticky-add-to-cart-bar.tsx
+++ b/src/components/sticky-add-to-cart-bar.tsx
@@ -25,7 +25,7 @@ export function StickyAddToCartBar({
product: ProductWithRelations;
inStock: boolean;
}) {
- const { t } = useI18n();
+ const { t, locale } = useI18n();
const addItem = useCartStore((s) => s.addItem);
const [justAdded, setJustAdded] = useState(false);
@@ -37,7 +37,7 @@ export function StickyAddToCartBar({
{product.name}
-
{formatPrice(product.priceCents, product.currency)}
+
{formatPrice(product.priceCents, product.currency, locale)}
{inStock ? (