Files
vula-7082e4ee/src/hooks/useVulaCart.ts
T
Vula Builder 04fa1eecd2 Deploy
2026-08-01 17:20:22 +00:00

111 lines
3.8 KiB
TypeScript

'use client'
import { useState, useEffect, useCallback } from 'react'
const COMMERCE_URL = process.env.NEXT_PUBLIC_VULA_COMMERCE_URL || 'https://ide.vulai.co.za'
const PROJECT_ID = process.env.NEXT_PUBLIC_VULA_PROJECT_ID || ''
const CART_KEY = `vula_cart_${PROJECT_ID}`
export interface VulaCartItem {
variantId: string
productId: string
title: string
variant_title: string
unit_price: number
quantity: number
thumbnail: string | null
}
export interface CheckoutDetails {
customerName: string
customerEmail: string
customerPhone?: string
fulfillmentType: string
shippingAddress?: { address_line1: string; city: string; province?: string; postal_code?: string }
notes?: string
}
function readCart(): VulaCartItem[] {
try { return JSON.parse(localStorage.getItem(CART_KEY) || '[]') } catch { return [] }
}
function writeCart(items: VulaCartItem[]) {
localStorage.setItem(CART_KEY, JSON.stringify(items))
// Notify all other useVulaCart instances in this tab to re-sync.
window.dispatchEvent(new CustomEvent('vula-cart-updated'))
}
export function useVulaCart() {
const [items, setItems] = useState<VulaCartItem[]>([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
setItems(readCart())
const onUpdate = () => setItems(readCart())
window.addEventListener('vula-cart-updated', onUpdate)
return () => window.removeEventListener('vula-cart-updated', onUpdate)
}, [])
const addToCart = useCallback((item: VulaCartItem) => {
// Read directly from localStorage (not from React state) so the side effect
// (writeCart + event dispatch) stays outside the state updater.
// React StrictMode calls updater functions twice, which would double-fire
// writeCart and cause duplicate items.
const current = readCart()
const exists = current.find(i => i.variantId === item.variantId)
const next = exists
? current.map(i => i.variantId === item.variantId ? { ...i, quantity: i.quantity + item.quantity } : i)
: [...current, item]
writeCart(next)
setItems(next)
}, [])
const removeFromCart = useCallback((variantId: string) => {
const next = readCart().filter(i => i.variantId !== variantId)
writeCart(next)
setItems(next)
}, [])
const updateQty = useCallback((variantId: string, qty: number) => {
const current = readCart()
const next = qty <= 0
? current.filter(i => i.variantId !== variantId)
: current.map(i => i.variantId === variantId ? { ...i, quantity: qty } : i)
writeCart(next)
setItems(next)
}, [])
// clearCart must also dispatch so the nav badge and other instances clear.
const clearCart = useCallback(() => {
localStorage.removeItem(CART_KEY)
window.dispatchEvent(new CustomEvent('vula-cart-updated'))
setItems([])
}, [])
const checkout = useCallback(async (details: CheckoutDetails) => {
setLoading(true); setError(null)
try {
const cart = readCart()
if (!cart.length) throw new Error('Your cart is empty')
const r = await fetch(`${COMMERCE_URL}/api/store/${PROJECT_ID}/checkout`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart, ...details })
})
const data = await r.json()
if (!data.success) throw new Error(data.error || 'Checkout failed')
clearCart()
return data.order
} catch (e: any) {
setError(e.message)
throw e
} finally {
setLoading(false)
}
}, [clearCart])
const subtotal = items.reduce((sum, i) => sum + i.unit_price * i.quantity, 0)
const itemCount = items.reduce((sum, i) => sum + i.quantity, 0)
return { items, subtotal, itemCount, loading, error, addToCart, removeFromCart, updateQty, clearCart, checkout }
}