cart-client.ts raw

   1  // single definition of the cart item shape - imported by every page script
   2  // that touches the cart. price_cents is authoritative naming; the server
   3  // re-prices from the catalog at checkout regardless.
   4  
   5  export interface CartItem {
   6    slug: string;
   7    name: string;
   8    name_en: string;
   9    price_cents: number;
  10    image: string;
  11    unit_size: number;
  12    unit: string;
  13    stock: number;
  14    qty: number;
  15  }
  16  
  17  const KEY = 'cart';
  18  
  19  export function getCart(): CartItem[] {
  20    try {
  21      const raw = JSON.parse(localStorage.getItem(KEY) || '[]');
  22      if (!Array.isArray(raw)) return [];
  23      return raw.filter(it => it && typeof it.slug === 'string' && Number.isFinite(it.price_cents) && Number.isInteger(it.qty) && it.qty > 0).map(it => ({
  24        ...it,
  25        unit_size: it.unit_size || 100,
  26        unit: it.unit || 'g',
  27        stock: it.stock || 99,
  28        image: resolveImage(it.image || ''),
  29        name_en: it.name_en || it.name || '',
  30      }));
  31    } catch {
  32      return [];
  33    }
  34  }
  35  
  36  function save(cart: CartItem[]) {
  37    localStorage.setItem(KEY, JSON.stringify(cart));
  38    window.dispatchEvent(new CustomEvent('cart-updated'));
  39  }
  40  
  41  function resolveImage(raw: string): string {
  42    try {
  43      const arr = JSON.parse(raw);
  44      return arr?.[0] || raw || '/images/logo.png';
  45    } catch { return raw || '/images/logo.png'; }
  46  }
  47  
  48  export function addToCart(item: Omit<CartItem, 'qty'>, qty = 1) {
  49    const cart = getCart();
  50    const existing = cart.find(it => it.slug === item.slug);
  51    const resolved = { ...item, unit_size: item.unit_size || 100, unit: item.unit || 'g', stock: item.stock || 99, image: resolveImage(item.image) };
  52    const limit = resolved.stock;
  53    if (existing) {
  54      existing.qty = Math.min(limit, existing.qty + qty);
  55    } else {
  56      cart.push({ ...resolved, qty: Math.min(limit, Math.max(1, qty)) });
  57    }
  58    save(cart);
  59  }
  60  
  61  export function setQty(index: number, qty: number) {
  62    const cart = getCart();
  63    if (!cart[index]) return;
  64    const limit = cart[index].stock;
  65    const newQty = Math.min(limit, Math.max(0, qty));
  66    if (newQty < 1) {
  67      cart.splice(index, 1);
  68    } else {
  69      cart[index].qty = newQty;
  70    }
  71    save(cart);
  72  }
  73  
  74  export function removeItem(index: number) {
  75    const cart = getCart();
  76    cart.splice(index, 1);
  77    save(cart);
  78  }
  79  
  80  export function clearCart() {
  81    save([]);
  82  }
  83  
  84  export function cartCount(): number {
  85    return getCart().reduce((sum, it) => sum + it.qty, 0);
  86  }
  87  
  88  export function cartSubtotalCents(): number {
  89    return getCart().reduce((sum, it) => sum + it.price_cents * it.qty, 0);
  90  }
  91  
  92  export function formatLv(cents: number): string {
  93    return (cents / 100).toFixed(2) + ' EUR';
  94  }
  95  
  96  export function escapeHtml(s: string): string {
  97    return s.replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]!));
  98  }
  99