Live Lighthouse + performance-trace audits against the running dev app found two real, measurable defects: - /checkout and /warenkorb rendered the cart-store's default empty state before Zustand's persist middleware finished reading localStorage, so any customer who already had items in their cart saw an "empty cart" flash before the real form appeared (CLS 0.82). Fixed by tracking hasHydrated in the store and rendering the existing route skeleton until rehydration completes. Verified via Chrome DevTools performance trace: CLS 0.82 -> 0.00 on both pages. - axe-core (via Lighthouse) flagged text-ink/60 on the cream background at 4.09:1 contrast, under the 4.5:1 WCAG AA minimum for normal-size text. Bumped to text-ink/70 (5.61:1) everywhere the same token is used at text-xs/text-sm across 7 files. Co-Authored-By: Claude Sonnet 5 <[email protected]>
90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { create } from "zustand";
|
|
import { persist } from "zustand/middleware";
|
|
|
|
export interface CartItem {
|
|
productId: string;
|
|
slug: string;
|
|
name: string;
|
|
weightGrams: number;
|
|
priceCents: number;
|
|
image: string | null;
|
|
quantity: number;
|
|
}
|
|
|
|
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<CartItem, "quantity">, quantity?: number) => void;
|
|
removeItem: (productId: string) => void;
|
|
setQuantity: (productId: string, quantity: number) => void;
|
|
clear: () => void;
|
|
}
|
|
|
|
export const useCartStore = create<CartState>()(
|
|
persist(
|
|
(set) => ({
|
|
items: [],
|
|
isOpen: false,
|
|
hasHydrated: false,
|
|
setHasHydrated: (hasHydrated) => set({ hasHydrated }),
|
|
openCart: () => set({ isOpen: true }),
|
|
closeCart: () => set({ isOpen: false }),
|
|
addItem: (item, quantity = 1) =>
|
|
set((state) => {
|
|
const existing = state.items.find((i) => i.productId === item.productId);
|
|
if (existing) {
|
|
return {
|
|
items: state.items.map((i) =>
|
|
i.productId === item.productId
|
|
? { ...i, quantity: i.quantity + quantity }
|
|
: i
|
|
),
|
|
isOpen: true,
|
|
};
|
|
}
|
|
return { items: [...state.items, { ...item, quantity }], isOpen: true };
|
|
}),
|
|
removeItem: (productId) =>
|
|
set((state) => ({
|
|
items: state.items.filter((i) => i.productId !== productId),
|
|
})),
|
|
setQuantity: (productId, quantity) =>
|
|
set((state) => ({
|
|
items: state.items
|
|
.map((i) => (i.productId === productId ? { ...i, quantity } : i))
|
|
.filter((i) => i.quantity > 0),
|
|
})),
|
|
clear: () => set({ items: [] }),
|
|
}),
|
|
{
|
|
name: "wabenhain-cart",
|
|
// Nur die Artikel persistieren — "isOpen" ist ein rein transienter
|
|
// 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);
|
|
},
|
|
}
|
|
)
|
|
);
|
|
|
|
export function cartTotalCents(items: CartItem[]): number {
|
|
return items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0);
|
|
}
|
|
|
|
export function cartItemCount(items: CartItem[]): number {
|
|
return items.reduce((sum, item) => sum + item.quantity, 0);
|
|
}
|