This commit is contained in:
Vula Builder
2026-08-01 17:20:22 +00:00
parent c943e792c3
commit 760d59c03c
+78
View File
@@ -0,0 +1,78 @@
'use client'
import { useState, useEffect } from 'react'
export interface VulaProductCategory {
id: string
name: string
handle: string
}
export interface VulaProduct {
id: string
title: string
description: string | null
thumbnail: string | null
created_at?: string
collection?: { id: string; title: string }
images?: Array<{ id?: string; url: string; position?: number }>
variants: Array<{
id: string
title: string
prices: Array<{ amount: number; currency_code: string }>
}>
categories?: VulaProductCategory[]
}
interface UseProductsResult {
products: VulaProduct[]
loading: boolean
isLoading: boolean // alias for loading — React Query convention, both work
error: string | null
}
type UseProductsOptions = number | { limit?: number; categoryId?: string }
// Accepts either a plain number or an options object — both call signatures are valid:
// useProducts(4)
// useProducts({ limit: 4, categoryId: 'cat_123' })
export function useProducts(options: UseProductsOptions = 12): UseProductsResult {
// Extract scalars so useEffect deps are stable primitives, not object references.
const limit = typeof options === 'number' ? options : (options.limit ?? 12)
const categoryId = typeof options === 'number' ? undefined : options.categoryId
const [products, setProducts] = useState<VulaProduct[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let controller = new AbortController()
const fetchProducts = () => {
controller = new AbortController()
const params = new URLSearchParams({ limit: String(limit) })
if (categoryId) params.set('categoryId', categoryId)
fetch(`/api/products?${params}`, { signal: controller.signal })
.then(res => res.ok ? res.json() : Promise.reject(res.status))
.then(data => {
setProducts(data.products ?? [])
setLoading(false)
})
.catch(err => { if (err?.name !== 'AbortError') { setError(String(err)); setLoading(false) } })
}
fetchProducts()
// Poll every 30 s so newly published products appear without a manual refresh.
const interval = setInterval(fetchProducts, 30000)
// Refetch immediately when the user switches back to this tab (e.g. from Business Studio).
const onVisible = () => { if (document.visibilityState === 'visible') fetchProducts() }
document.addEventListener('visibilitychange', onVisible)
return () => {
clearInterval(interval)
controller.abort()
document.removeEventListener('visibilitychange', onVisible)
}
}, [limit, categoryId])
return { products, loading, isLoading: loading, error }
}