111 lines
3.8 KiB
TypeScript
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 }
|
|
}
|