diff --git a/src/app/api/shop/search-suggestions/route.ts b/src/app/api/shop/search-suggestions/route.ts index ea8ae44..6c57a6c 100644 --- a/src/app/api/shop/search-suggestions/route.ts +++ b/src/app/api/shop/search-suggestions/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { getActiveProducts } from "@/lib/data"; import { formatPrice } from "@/lib/format"; import { rateLimit, getClientIp } from "@/lib/rate-limit"; +import { getLocale } from "@/i18n/locale"; const SUGGESTION_LIMIT = 5; const SUGGEST_ATTEMPTS_PER_WINDOW = 60; @@ -26,11 +27,12 @@ export async function GET(request: Request) { return NextResponse.json({ suggestions: [] }); } + const locale = await getLocale(); const products = await getActiveProducts({ query, onlyAvailable: true }); const suggestions = products.slice(0, SUGGESTION_LIMIT).map((product) => ({ slug: product.slug, name: product.name, - price: formatPrice(product.priceCents, product.currency), + price: formatPrice(product.priceCents, product.currency, locale), })); return NextResponse.json({ suggestions }); diff --git a/src/app/bestellung-verfolgen/page.tsx b/src/app/bestellung-verfolgen/page.tsx index d1fc6db..5722cbc 100644 --- a/src/app/bestellung-verfolgen/page.tsx +++ b/src/app/bestellung-verfolgen/page.tsx @@ -16,7 +16,8 @@ const TRACK_ATTEMPTS_PER_WINDOW = 10; const TRACK_WINDOW_MS = 15 * 60 * 1000; export default async function TrackOrderPage(props: PageProps<"/bestellung-verfolgen">) { - const t = getDictionary(await getLocale()); + const locale = await getLocale(); + const t = getDictionary(locale); const { order: orderNumber, email } = await props.searchParams; const hasQuery = typeof orderNumber === "string" && typeof email === "string" && orderNumber && email; @@ -80,7 +81,7 @@ export default async function TrackOrderPage(props: PageProps<"/bestellung-verfo

{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}

@@ -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 (
- {descriptiveAlt} + {descriptiveAlt}
); } 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. - {mainImageAlt} + // 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. + {mainImageAlt} ) : ( )} @@ -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 ? (