diff --git a/src/app/charge/[code]/page.tsx b/src/app/charge/[code]/page.tsx index b7fe17f..dca6e50 100644 --- a/src/app/charge/[code]/page.tsx +++ b/src/app/charge/[code]/page.tsx @@ -49,7 +49,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {

{batch.harvest.type} {batch.harvest.year}

-

{t.chargePage.provenanceNote(batch.code)}

+

{t.chargePage.provenanceNote(batch.code)}

) {
-

{step.label}

+

{step.label}

{step.value}

))} @@ -86,7 +86,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) {
-

{t.chargePage.regionLabel}

+

{t.chargePage.regionLabel}

{batch.harvest.location}

{batch.harvest.description}

@@ -95,7 +95,7 @@ export default async function BatchPage(props: PageProps<"/charge/[code]">) { {batch.products.length > 0 ? (
-

{t.chargePage.fromThisBatch}

+

{t.chargePage.fromThisBatch}

{batch.products.map((product) => ( s.items); + const hasHydrated = useCartStore((s) => s.hasHydrated); const subtotal = cartTotalCents(items); const totalQuantity = cartItemCount(items); const router = useRouter(); @@ -109,6 +111,15 @@ export function CheckoutForm({ } } + // Der Cart-Store haelt seine Positionen nur im Browser (persist/localStorage) + // und liest sie erst nach dem ersten Client-Render nach. Ohne diese Weiche + // wuerde jede:r Kund:in mit gefuelltem Warenkorb kurz die "leer"-Meldung + // sehen, bevor auf das echte Formular umgeschaltet wird — ein grosser, + // messbarer Layout-Sprung (siehe CLS) und verwirrend fuer echte Kaeufer:innen. + if (!hasHydrated) { + return ; + } + if (items.length === 0) { return (
@@ -179,7 +190,7 @@ export function CheckoutForm({ ))}
{fulfillment === "VERSAND" ? ( -

+

{t.checkoutPage.deliveryTimeNote}

) : null} diff --git a/src/app/warenkorb/warenkorb-content.tsx b/src/app/warenkorb/warenkorb-content.tsx index a499fec..88e38f4 100644 --- a/src/app/warenkorb/warenkorb-content.tsx +++ b/src/app/warenkorb/warenkorb-content.tsx @@ -7,6 +7,7 @@ import { CartItemThumbnail } from "@/components/cart-item-thumbnail"; import { QuantityStepper } from "@/components/quantity-stepper"; import { ProductCard } from "@/components/product-card"; import { useCartStore, cartTotalCents, cartItemCount } from "@/store/cart-store"; +import WarenkorbLoading from "./loading"; import { useCartStock } from "@/hooks/use-cart-stock"; import { formatPrice } from "@/lib/format"; import { resolveShippingCostCents, getNextShippingTier, type ShippingTierLike } from "@/lib/shipping-calc"; @@ -27,6 +28,7 @@ export function WarenkorbContent({ }) { const { t, locale } = useI18n(); const items = useCartStore((s) => s.items); + const hasHydrated = useCartStore((s) => s.hasHydrated); const setQuantity = useCartStore((s) => s.setQuantity); const removeItem = useCartStore((s) => s.removeItem); // Live-Bestand je Position — derselbe Hinweis wie auf der Produktkarte/ @@ -53,6 +55,12 @@ export function WarenkorbContent({ .filter((product) => !cartProductIds.has(product.id)) .slice(0, CROSS_SELL_LIMIT); + // Siehe checkout-form.tsx: ohne diese Weiche flackert kurz die + // "leer"-Meldung auf, bevor der Cart-Store aus localStorage nachliest. + if (!hasHydrated) { + return ; + } + return (
@@ -122,7 +130,7 @@ export function WarenkorbContent({ {shipping === 0 ? t.cartPage.free : formatPrice(shipping, "EUR", locale)}
{nextTier && nextTier.unitsUntilNext <= 2 ? ( -

+

{t.cartPage.nextTierHint(nextTier.unitsUntilNext, formatPrice(nextTier.priceCents, "EUR", locale))}

) : null} @@ -134,7 +142,7 @@ export function WarenkorbContent({ -{formatPrice(bulkDiscountCents, "EUR", locale)}
) : nextBulkDiscount && nextBulkDiscount.unitsUntilNext <= 2 ? ( -

+

{t.cartPage.bulkDiscountProgress(nextBulkDiscount.unitsUntilNext, nextBulkDiscount.percent)}

) : null} @@ -146,7 +154,7 @@ export function WarenkorbContent({ {t.cart.checkout} - + {t.cartPage.pickupHint} diff --git a/src/app/wissen/[slug]/page.tsx b/src/app/wissen/[slug]/page.tsx index de79700..3a88636 100644 --- a/src/app/wissen/[slug]/page.tsx +++ b/src/app/wissen/[slug]/page.tsx @@ -86,7 +86,7 @@ export default async function WissenArticlePage( {otherArticles.length > 0 ? (
-

{t.wissenArticlePage.otherArticlesHeading}

+

{t.wissenArticlePage.otherArticlesHeading}

{otherArticles.map((otherSlug) => { const other = t.wissenArticles[otherSlug]; diff --git a/src/components/password-field.tsx b/src/components/password-field.tsx index 3a31352..83858f9 100644 --- a/src/components/password-field.tsx +++ b/src/components/password-field.tsx @@ -123,7 +123,7 @@ export function PasswordField({ /> ))} - {tierLabel} + {tierLabel} ) : null} diff --git a/src/components/product-card.tsx b/src/components/product-card.tsx index 71a63ef..dbdd72d 100644 --- a/src/components/product-card.tsx +++ b/src/components/product-card.tsx @@ -45,7 +45,7 @@ export function ProductTeaserLink({ {name} - + {t.product.viewProduct} diff --git a/src/components/site-chrome.tsx b/src/components/site-chrome.tsx index 5e4f948..1336d78 100644 --- a/src/components/site-chrome.tsx +++ b/src/components/site-chrome.tsx @@ -163,7 +163,7 @@ function SearchButton() { className="flex items-center justify-between gap-4 px-4 py-2.5 text-sm text-ink hover:bg-cream/60" > {s.name} - {s.price} + {s.price} ))} diff --git a/src/store/cart-store.ts b/src/store/cart-store.ts index 7ff8cc7..5d5143a 100644 --- a/src/store/cart-store.ts +++ b/src/store/cart-store.ts @@ -16,6 +16,13 @@ export interface CartItem { interface CartState { items: CartItem[]; isOpen: boolean; + // Client-only: waehrend des Server-Renders und des allerersten Client- + // Renders (bevor persist localStorage gelesen hat) ist dies immer false, + // damit UI, die auf "items.length === 0" verzweigt (Checkout/Warenkorb), + // nicht kurz einen falschen "leerer Warenkorb"-Zustand zeigt, bevor die + // echten, aus localStorage wiederhergestellten Positionen ankommen. + hasHydrated: boolean; + setHasHydrated: (hasHydrated: boolean) => void; openCart: () => void; closeCart: () => void; addItem: (item: Omit, quantity?: number) => void; @@ -29,6 +36,8 @@ export const useCartStore = create()( (set) => ({ items: [], isOpen: false, + hasHydrated: false, + setHasHydrated: (hasHydrated) => set({ hasHydrated }), openCart: () => set({ isOpen: true }), closeCart: () => set({ isOpen: false }), addItem: (item, quantity = 1) => @@ -64,6 +73,9 @@ export const useCartStore = create()( // UI-Zustand und darf nach einem Seiten-Reload nicht wiederhergestellt // werden (sonst poppt das Cart-Drawer nach jedem Reload erneut auf). partialize: (state) => ({ items: state.items }), + onRehydrateStorage: () => (state) => { + state?.setHasHydrated(true); + }, } ) );