484 lines
17 KiB
TypeScript
484 lines
17 KiB
TypeScript
"use client"
|
||
|
||
import { useState, useEffect } from "react"
|
||
import Image from "next/image"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import SquarePaymentForm from "@/components/square-payment-form"
|
||
import SiteFooter from "@/components/site-footer"
|
||
|
||
interface Product {
|
||
id: string
|
||
variationId: string
|
||
name: string
|
||
description: string
|
||
price: number
|
||
currency: string
|
||
imageUrl: string
|
||
inventory: number
|
||
category: string
|
||
}
|
||
|
||
interface ApiResponse {
|
||
success: boolean
|
||
products?: Product[]
|
||
error?: string
|
||
message?: string
|
||
details?: any
|
||
}
|
||
|
||
export default function Shop() {
|
||
const [products, setProducts] = useState<Product[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [error, setError] = useState<string>("")
|
||
const [cart, setCart] = useState<
|
||
Array<{
|
||
id: string
|
||
variationId: string
|
||
name: string
|
||
price: number
|
||
quantity: number
|
||
description?: string
|
||
}>
|
||
>([])
|
||
const [showCheckout, setShowCheckout] = useState(false)
|
||
const [orderComplete, setOrderComplete] = useState(false)
|
||
|
||
useEffect(() => {
|
||
fetchProducts()
|
||
}, [])
|
||
|
||
const fetchProducts = async () => {
|
||
try {
|
||
setLoading(true)
|
||
setError("")
|
||
|
||
console.log("Fetching products from /api/square/catalog...")
|
||
|
||
// First check Square health
|
||
const healthResponse = await fetch("/api/square/health")
|
||
const healthData = await healthResponse.json()
|
||
|
||
if (!healthData.success) {
|
||
console.error("Square health check failed:", healthData)
|
||
setError(`Square API configuration issue: ${healthData.error || 'Connection failed'}`)
|
||
return
|
||
}
|
||
|
||
console.log("Square health check passed, fetching catalog...")
|
||
|
||
const response = await fetch("/api/square/catalog?includeInventory=true")
|
||
|
||
console.log("Response status:", response.status)
|
||
console.log("Response headers:", Object.fromEntries(response.headers.entries()))
|
||
|
||
// Check if response is ok
|
||
if (!response.ok) {
|
||
const errorText = await response.text()
|
||
console.error("HTTP error:", response.status, errorText.substring(0, 500))
|
||
setError(`Server error (${response.status}): ${response.statusText}`)
|
||
return
|
||
}
|
||
|
||
// Check if response is JSON
|
||
const contentType = response.headers.get("content-type")
|
||
if (!contentType || !contentType.includes("application/json")) {
|
||
const responseText = await response.text()
|
||
console.error("Non-JSON response:", responseText.substring(0, 500))
|
||
setError("Server returned invalid response format. Check server logs for details.")
|
||
return
|
||
}
|
||
|
||
const data: ApiResponse = await response.json()
|
||
console.log("API Response:", data)
|
||
|
||
if (data.success && data.products) {
|
||
setProducts(data.products)
|
||
console.log("Products loaded:", data.products.length)
|
||
} else {
|
||
const errorMsg = data.error || data.message || "Failed to load products"
|
||
console.error("API returned error:", errorMsg)
|
||
setError(errorMsg)
|
||
|
||
// Show additional details if available
|
||
if (data.details) {
|
||
console.error("Error details:", data.details)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Error fetching products:", error)
|
||
|
||
// More specific error handling
|
||
if (error instanceof TypeError && error.message.includes("json")) {
|
||
setError("Server returned invalid data format. This usually means Square SDK failed to load.")
|
||
} else if (error instanceof TypeError && error.message.includes("fetch")) {
|
||
setError("Network connection failed. Please check your internet connection.")
|
||
} else {
|
||
setError(`Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`)
|
||
}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const addToCart = (product: Product) => {
|
||
if (product.inventory <= 0) {
|
||
alert("Sorry, this item is out of stock!")
|
||
return
|
||
}
|
||
|
||
setCart((prev) => {
|
||
const existing = prev.find((item) => item.id === product.id)
|
||
if (existing) {
|
||
const newQuantity = existing.quantity + 1
|
||
if (newQuantity > product.inventory) {
|
||
alert(`Sorry, only ${product.inventory} items available in stock!`)
|
||
return prev
|
||
}
|
||
return prev.map((item) => (item.id === product.id ? { ...item, quantity: newQuantity } : item))
|
||
}
|
||
return [
|
||
...prev,
|
||
{
|
||
id: product.id,
|
||
variationId: product.variationId,
|
||
name: product.name,
|
||
price: product.price,
|
||
quantity: 1,
|
||
description: product.description,
|
||
},
|
||
]
|
||
})
|
||
}
|
||
|
||
const removeFromCart = (productId: string) => {
|
||
setCart((prev) => prev.filter((item) => item.id !== productId))
|
||
}
|
||
|
||
const updateQuantity = (productId: string, quantity: number) => {
|
||
if (quantity === 0) {
|
||
removeFromCart(productId)
|
||
return
|
||
}
|
||
|
||
const product = products.find((p) => p.id === productId)
|
||
if (product && quantity > product.inventory) {
|
||
alert(`Sorry, only ${product.inventory} items available in stock!`)
|
||
return
|
||
}
|
||
|
||
setCart((prev) => prev.map((item) => (item.id === productId ? { ...item, quantity } : item)))
|
||
}
|
||
|
||
const handlePaymentSuccess = (paymentResult: any) => {
|
||
console.log("Payment successful:", paymentResult)
|
||
setOrderComplete(true)
|
||
setCart([])
|
||
setShowCheckout(false)
|
||
fetchProducts()
|
||
}
|
||
|
||
const handlePaymentError = (error: string) => {
|
||
console.error("Payment error:", error)
|
||
alert(`Payment failed: ${error}`)
|
||
}
|
||
|
||
if (orderComplete) {
|
||
return (
|
||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||
<Card className="max-w-md mx-auto">
|
||
<CardContent className="text-center p-8">
|
||
<div className="text-6xl mb-4">✨</div>
|
||
<h2 className="text-2xl font-bold mb-4">Order Complete!</h2>
|
||
<p className="text-gray-600 mb-6">
|
||
Thank you for your purchase! You'll receive a confirmation email shortly.
|
||
</p>
|
||
<Button onClick={() => setOrderComplete(false)}>Continue Shopping</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (showCheckout && cart.length > 0) {
|
||
return (
|
||
<div className="min-h-screen bg-black">
|
||
<header className="bg-[#e8e4d3] py-4">
|
||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||
<div className="flex items-center">
|
||
<a href="/" className="flex items-center">
|
||
<Image
|
||
src="/images/logo-black-white.png"
|
||
alt="Aunty Sparkles Logo"
|
||
width={96}
|
||
height={96}
|
||
className="mr-4"
|
||
/>
|
||
</a>
|
||
</div>
|
||
<div className="flex items-center space-x-8">
|
||
<nav>
|
||
<ul className="flex space-x-8 text-xl font-bold">
|
||
<li>
|
||
<a href="/about" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||
About
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="/gallery" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||
Gallery
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="/contact" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||
Contact
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
<div className="w-8"></div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<section className="section-dark py-20">
|
||
<div className="container mx-auto px-4">
|
||
<div className="flex items-center justify-between mb-8">
|
||
<h1 className="text-4xl font-bold text-white">Checkout</h1>
|
||
<Button
|
||
onClick={() => setShowCheckout(false)}
|
||
variant="outline"
|
||
className="text-white border-white hover:bg-white hover:text-black"
|
||
>
|
||
Back to Shop
|
||
</Button>
|
||
</div>
|
||
|
||
<SquarePaymentForm
|
||
items={cart}
|
||
onPaymentSuccess={handlePaymentSuccess}
|
||
onPaymentError={handlePaymentError}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<SiteFooter />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-black">
|
||
{/* Header */}
|
||
<header className="bg-[#e8e4d3] py-4">
|
||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||
<div className="flex items-center">
|
||
<a href="/" className="flex items-center">
|
||
<Image
|
||
src="/images/logo-black-white.png"
|
||
alt="Aunty Sparkles Logo"
|
||
width={96}
|
||
height={96}
|
||
className="mr-4"
|
||
/>
|
||
</a>
|
||
</div>
|
||
<div className="flex items-center space-x-8">
|
||
<nav>
|
||
<ul className="flex space-x-8 text-xl font-bold">
|
||
<li>
|
||
<a href="/about" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||
About
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="/gallery" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||
Gallery
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="/contact" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||
Contact
|
||
</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
<div className="w-8">
|
||
{cart.length > 0 && (
|
||
<div className="relative">
|
||
<Button
|
||
onClick={() => setShowCheckout(true)}
|
||
className="bg-yellow-400 text-black hover:bg-yellow-300 text-xs px-2 py-1"
|
||
>
|
||
Cart ({cart.reduce((sum, item) => sum + item.quantity, 0)})
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Hero Section */}
|
||
<section className="hero-bg py-20">
|
||
<div className="container mx-auto px-4 text-center">
|
||
<h1 className="text-5xl font-bold text-white mb-4">Shop</h1>
|
||
<div className="w-16 h-1 bg-yellow-400 mb-8 mx-auto"></div>
|
||
<p className="text-lg text-gray-300 max-w-2xl mx-auto">
|
||
Discover unique, handcrafted pieces that tell a story. Each item is lovingly upcycled and designed to help
|
||
you sparkle! ✨
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Debug Info */}
|
||
{error && (
|
||
<section className="bg-red-900 py-4">
|
||
<div className="container mx-auto px-4 text-center">
|
||
<p className="text-white font-semibold">Debug Info: {error}</p>
|
||
<p className="text-red-200 text-sm mt-2">
|
||
Check browser console for more details. This usually means Square API credentials need to be configured.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Products Grid */}
|
||
<section className="section-dark py-20">
|
||
<div className="container mx-auto px-4">
|
||
{loading ? (
|
||
<div className="text-center text-white">
|
||
<div className="text-2xl mb-4">Loading products...</div>
|
||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-yellow-400 mx-auto"></div>
|
||
<p className="text-gray-300 mt-4">Connecting to Square catalog...</p>
|
||
</div>
|
||
) : error ? (
|
||
<div className="text-center text-white">
|
||
<div className="text-2xl mb-4">Unable to load products</div>
|
||
<p className="text-gray-300 mb-4">{error}</p>
|
||
<div className="space-y-2 mb-4">
|
||
<p className="text-sm text-gray-400">Common solutions:</p>
|
||
<ul className="text-sm text-gray-400 list-disc list-inside">
|
||
<li>Check that Square API credentials are configured in environment variables</li>
|
||
<li>Verify that products exist in your Square catalog</li>
|
||
<li>Ensure Square API access token has catalog permissions</li>
|
||
</ul>
|
||
</div>
|
||
<button
|
||
onClick={fetchProducts}
|
||
className="bg-yellow-400 text-black px-6 py-2 rounded hover:bg-yellow-300 transition-colors"
|
||
>
|
||
Try Again
|
||
</button>
|
||
</div>
|
||
) : products.length === 0 ? (
|
||
<div className="text-center text-white">
|
||
<div className="text-2xl mb-4">No products available</div>
|
||
<p className="text-gray-300">Add products to your Square catalog to see them here.</p>
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||
{products.map((product) => (
|
||
<Card key={product.id} className="bg-gray-800 border-gray-700">
|
||
<CardHeader className="p-0 relative">
|
||
<Image
|
||
src={product.imageUrl || "/placeholder.svg"}
|
||
alt={product.name}
|
||
width={400}
|
||
height={400}
|
||
className="w-full h-64 object-cover rounded-t-lg"
|
||
/>
|
||
{product.inventory <= 0 && (
|
||
<Badge className="absolute top-2 right-2 bg-red-500">Out of Stock</Badge>
|
||
)}
|
||
{product.inventory > 0 && product.inventory <= 5 && (
|
||
<Badge className="absolute top-2 right-2 bg-orange-500">Only {product.inventory} left</Badge>
|
||
)}
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
<CardTitle className="text-white mb-2">{product.name}</CardTitle>
|
||
<p className="text-gray-300 text-sm mb-4 line-clamp-3">{product.description}</p>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-2xl font-bold text-yellow-400">${product.price.toFixed(2)}</span>
|
||
<Button
|
||
onClick={() => addToCart(product)}
|
||
disabled={product.inventory <= 0}
|
||
className="bg-pink-500 text-white hover:bg-pink-400 disabled:bg-gray-500 disabled:cursor-not-allowed"
|
||
>
|
||
{product.inventory <= 0 ? "Out of Stock" : "Add to Cart"}
|
||
</Button>
|
||
</div>
|
||
<div className="mt-2 text-sm text-gray-400">
|
||
{product.inventory > 5 ? "In Stock" : `${product.inventory} in stock`}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Cart Summary */}
|
||
{cart.length > 0 && (
|
||
<section className="section-dark py-10 border-t border-gray-700">
|
||
<div className="container mx-auto px-4">
|
||
<div className="max-w-md mx-auto">
|
||
<h3 className="text-xl font-bold text-white mb-4">Shopping Cart</h3>
|
||
<div className="space-y-2 mb-4">
|
||
{cart.map((item) => (
|
||
<div key={item.id} className="flex items-center justify-between text-white">
|
||
<span className="text-sm">
|
||
{item.name} x{item.quantity}
|
||
</span>
|
||
<div className="flex items-center space-x-2">
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => updateQuantity(item.id, item.quantity - 1)}
|
||
className="h-6 w-6 p-0"
|
||
>
|
||
-
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||
className="h-6 w-6 p-0"
|
||
>
|
||
+
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="destructive"
|
||
onClick={() => removeFromCart(item.id)}
|
||
className="h-6 w-6 p-0"
|
||
>
|
||
×
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<span className="text-white font-bold">
|
||
Total: ${cart.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2)}
|
||
</span>
|
||
</div>
|
||
<Button
|
||
onClick={() => setShowCheckout(true)}
|
||
className="w-full bg-yellow-400 text-black hover:bg-yellow-300"
|
||
>
|
||
Proceed to Checkout
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
<SiteFooter />
|
||
</div>
|
||
)
|
||
}
|