90be4696af
Matches the Figma mockup (checkmark hero, order summary card,
delivery-status panel, testimonial band) with the checkout's 4-step
bar inserted (all steps done) and the "Bis dahin: Lass dich
inspirieren" block omitted, per request.
Extracted the step bar into a shared CheckoutSteps component so
/checkout and /bestellbestaetigung don't duplicate it. The actually-
selected shipping cost and payment method are captured into a
sessionStorage snapshot by /checkout's "Jetzt kaufen" click (there's
no real order backend, so this click is what "placing the order"
means here) and read back on the confirmation page — not just
defaulted to the first active method of each, so the receipt matches
what the shopper actually picked. Also tightened the checkout
newsletter-consent copy ("Wenn du zustimmst" instead of "Wenn du
oben zustimmst").
109 lines
3.5 KiB
TypeScript
109 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { useSyncExternalStore } from "react";
|
|
|
|
const CART_KEY = "ep_cart";
|
|
const CART_EVENT = "ep-cart-updated";
|
|
const EMPTY_CART: CartItem[] = [];
|
|
|
|
export type CartItem = { id: string; qty: number };
|
|
|
|
function readCart(): CartItem[] {
|
|
if (typeof window === "undefined") return [];
|
|
try {
|
|
const raw = window.localStorage.getItem(CART_KEY);
|
|
return raw ? JSON.parse(raw) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function writeCart(items: CartItem[]) {
|
|
window.localStorage.setItem(CART_KEY, JSON.stringify(items));
|
|
window.dispatchEvent(new Event(CART_EVENT));
|
|
}
|
|
|
|
export function getCartCount(): number {
|
|
return readCart().reduce((sum, item) => sum + item.qty, 0);
|
|
}
|
|
|
|
export function addToCart(id: string, qty = 1) {
|
|
const items = readCart();
|
|
const existing = items.find((i) => i.id === id);
|
|
if (existing) existing.qty += qty;
|
|
else items.push({ id, qty });
|
|
writeCart(items);
|
|
}
|
|
|
|
export function removeFromCart(id: string) {
|
|
writeCart(readCart().filter((i) => i.id !== id));
|
|
}
|
|
|
|
// Called by /bestellbestaetigung once it has captured a snapshot of the
|
|
// cart to display — there's no real order backend here, so "placing an
|
|
// order" just means the local cart empties out the same way it would
|
|
// after a real purchase completes.
|
|
export function clearCart() {
|
|
writeCart([]);
|
|
}
|
|
|
|
// qty <= 0 removes the item outright — the cart page's quantity stepper
|
|
// never lets the visible count go below 1, but this keeps the function
|
|
// itself safe to call with any integer without a separate remove path.
|
|
export function setQuantity(id: string, qty: number) {
|
|
if (qty <= 0) {
|
|
removeFromCart(id);
|
|
return;
|
|
}
|
|
const items = readCart();
|
|
const existing = items.find((i) => i.id === id);
|
|
if (existing) existing.qty = qty;
|
|
writeCart(items);
|
|
}
|
|
|
|
// Cached-by-raw-string snapshot, not a fresh JSON.parse() every call —
|
|
// useSyncExternalStore (below) requires getSnapshot to return the SAME
|
|
// reference when the underlying data hasn't actually changed, or React
|
|
// treats every render as a change. readCart()'s plain JSON.parse would
|
|
// allocate a new array each call and break that.
|
|
let cachedRaw: string | null | undefined;
|
|
let cachedItems: CartItem[] = EMPTY_CART;
|
|
|
|
export function getCart(): CartItem[] {
|
|
if (typeof window === "undefined") return EMPTY_CART;
|
|
const raw = window.localStorage.getItem(CART_KEY);
|
|
if (raw === cachedRaw) return cachedItems;
|
|
cachedRaw = raw;
|
|
try {
|
|
cachedItems = raw ? JSON.parse(raw) : EMPTY_CART;
|
|
} catch {
|
|
cachedItems = EMPTY_CART;
|
|
}
|
|
return cachedItems;
|
|
}
|
|
|
|
function subscribe(onStoreChange: () => void) {
|
|
window.addEventListener(CART_EVENT, onStoreChange);
|
|
window.addEventListener("storage", onStoreChange);
|
|
return () => {
|
|
window.removeEventListener(CART_EVENT, onStoreChange);
|
|
window.removeEventListener("storage", onStoreChange);
|
|
};
|
|
}
|
|
|
|
// useSyncExternalStore, not useState+useEffect — localStorage is an
|
|
// external store outside React, and the previous approach (setState
|
|
// synchronously inside an effect body) causes an extra render and trips
|
|
// the react-hooks/set-state-in-effect lint rule. This is React's own
|
|
// recommended pattern for subscribing to exactly this kind of external
|
|
// store, and is correctly SSR-safe via the third (server snapshot)
|
|
// argument — 0 / EMPTY_CART until the client hydrates and reads
|
|
// localStorage for real.
|
|
export function useCartCount(): number {
|
|
return useSyncExternalStore(subscribe, getCartCount, () => 0);
|
|
}
|
|
|
|
export function useCart(): CartItem[] {
|
|
return useSyncExternalStore(subscribe, getCart, () => EMPTY_CART);
|
|
}
|