247 lines
8.0 KiB
TypeScript
247 lines
8.0 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Heart, ShoppingCart, Trash2, X } from 'lucide-react';
|
|
import { api } from '@/lib/api/client';
|
|
import { Button } from '@/components/ui/button';
|
|
import { formatPrice } from '@/lib/currency';
|
|
import { toast } from 'sonner';
|
|
import { useModules } from '@/hooks/useModules';
|
|
import { useModuleSettings } from '@/hooks/useModuleSettings';
|
|
import SEOHead from '@/components/SEOHead';
|
|
|
|
interface WishlistItem {
|
|
product_id: number;
|
|
name: string;
|
|
slug: string;
|
|
price: string;
|
|
regular_price?: string;
|
|
sale_price?: string;
|
|
image?: string;
|
|
on_sale?: boolean;
|
|
stock_status?: string;
|
|
type?: string;
|
|
added_at: string;
|
|
}
|
|
|
|
export default function Wishlist() {
|
|
const navigate = useNavigate();
|
|
const [items, setItems] = useState<WishlistItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const { isEnabled, isLoading: modulesLoading } = useModules();
|
|
const { settings: wishlistSettings } = useModuleSettings('wishlist');
|
|
|
|
useEffect(() => {
|
|
if (isEnabled('wishlist')) {
|
|
loadWishlist();
|
|
}
|
|
}, [isEnabled]);
|
|
|
|
if (modulesLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isEnabled('wishlist')) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center px-4">
|
|
<div className="max-w-md w-full bg-yellow-50 border border-yellow-200 rounded-lg p-8 text-center">
|
|
<Heart className="h-16 w-16 text-yellow-600 mx-auto mb-4" />
|
|
<h2 className="text-2xl font-bold mb-2">Wishlist Not Available</h2>
|
|
<p className="text-gray-600 mb-6">
|
|
The wishlist feature is currently disabled.
|
|
</p>
|
|
<Button onClick={() => navigate('/')} className="w-full">
|
|
Continue Shopping
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const loadWishlist = async () => {
|
|
try {
|
|
const data = await api.get<WishlistItem[]>('/account/wishlist');
|
|
setItems(data);
|
|
} catch (error) {
|
|
console.error('Failed to load wishlist:', error);
|
|
toast.error('Failed to load wishlist');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRemove = async (productId: number) => {
|
|
try {
|
|
await api.delete(`/account/wishlist/${productId}`);
|
|
setItems(items.filter(item => item.product_id !== productId));
|
|
toast.success('Removed from wishlist');
|
|
} catch (error) {
|
|
console.error('Failed to remove from wishlist:', error);
|
|
toast.error('Failed to remove from wishlist');
|
|
}
|
|
};
|
|
|
|
const handleAddToCart = async (item: WishlistItem) => {
|
|
if (item.type === 'variable') {
|
|
navigate(`/product/${item.slug}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api.post('/cart/add', {
|
|
product_id: item.product_id,
|
|
quantity: 1,
|
|
});
|
|
toast.success('Added to cart');
|
|
} catch (error) {
|
|
console.error('Failed to add to cart:', error);
|
|
toast.error('Failed to add to cart');
|
|
}
|
|
};
|
|
|
|
const handleClearAll = async () => {
|
|
if (!confirm('Are you sure you want to clear your entire wishlist?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await api.post('/account/wishlist/clear', {});
|
|
setItems([]);
|
|
toast.success('Wishlist cleared');
|
|
} catch (error) {
|
|
console.error('Failed to clear wishlist:', error);
|
|
toast.error('Failed to clear wishlist');
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<SEOHead title="Wishlist" description="Your saved products" />
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h1 className="text-2xl font-bold">My Wishlist</h1>
|
|
<p className="text-gray-600 text-sm mt-1">
|
|
{items.length} {items.length === 1 ? 'item' : 'items'}
|
|
</p>
|
|
</div>
|
|
{items.length > 0 && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleClearAll}
|
|
className="text-red-600 hover:text-red-700 hover:border-red-600"
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
Clear All
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Empty State */}
|
|
{items.length === 0 ? (
|
|
<div className="text-center py-16 bg-white border rounded-lg">
|
|
<Heart className="w-16 h-16 mx-auto text-gray-300 mb-4" />
|
|
<h2 className="text-xl font-semibold mb-2">Your wishlist is empty</h2>
|
|
<p className="text-gray-600 mb-6">
|
|
Save your favorite products to buy them later
|
|
</p>
|
|
<Button onClick={() => navigate('/shop')}>
|
|
Browse Products
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
/* Wishlist Grid */
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{items.map((item) => (
|
|
<div
|
|
key={item.product_id}
|
|
className="bg-white border rounded-lg overflow-hidden hover:shadow-lg transition-shadow group"
|
|
>
|
|
{/* Image */}
|
|
<div className="relative aspect-square bg-gray-100">
|
|
<button
|
|
onClick={() => handleRemove(item.product_id)}
|
|
className="absolute top-3 right-3 z-10 p-2 bg-white rounded-full shadow-md hover:bg-red-50 transition-colors"
|
|
title="Remove from wishlist"
|
|
>
|
|
<X className="w-4 h-4 text-red-600" />
|
|
</button>
|
|
|
|
<img
|
|
src={item.image || '/placeholder.png'}
|
|
alt={item.name}
|
|
className="w-full h-full object-cover cursor-pointer"
|
|
onClick={() => navigate(`/product/${item.slug}`)}
|
|
/>
|
|
|
|
{item.on_sale && (
|
|
<div className="absolute top-3 left-3 bg-red-600 text-white text-xs font-bold px-2 py-1 rounded">
|
|
SALE
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-4">
|
|
<h3
|
|
className="font-medium text-gray-900 mb-2 line-clamp-2 cursor-pointer hover:text-primary transition-colors"
|
|
onClick={() => navigate(`/product/${item.slug}`)}
|
|
>
|
|
{item.name}
|
|
</h3>
|
|
|
|
{/* Price */}
|
|
<div className="flex items-center gap-2 mb-4">
|
|
{item.on_sale && item.regular_price ? (
|
|
<>
|
|
<span className="text-lg font-bold text-primary">
|
|
{formatPrice(item.sale_price || item.price)}
|
|
</span>
|
|
<span className="text-sm text-gray-500 line-through">
|
|
{formatPrice(item.regular_price)}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="text-lg font-bold text-gray-900">
|
|
{formatPrice(item.price)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{(wishlistSettings.show_add_to_cart_button ?? true) && (
|
|
<Button
|
|
onClick={() => handleAddToCart(item)}
|
|
disabled={item.stock_status === 'outofstock'}
|
|
className="w-full"
|
|
size="sm"
|
|
>
|
|
<ShoppingCart className="w-4 h-4 mr-2" />
|
|
{item.stock_status === 'outofstock'
|
|
? 'Out of Stock'
|
|
: item.type === 'variable'
|
|
? 'Select Options'
|
|
: 'Add to Cart'}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|