veza/apps/web/src/components/commerce/WishlistView.tsx

247 lines
8.1 KiB
TypeScript
Raw Normal View History

import { Button } from '../ui/button';
import { Card } from '../ui/card';
import { EmptyState } from '../ui/empty-state';
import { Skeleton } from '../ui/skeleton';
import { useCartStore } from '../../stores/cartStore';
import { Product } from '../../types';
import { Heart, Pause, Play, ShoppingCart, Trash2, Zap } from 'lucide-react';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { useToast } from '../../components/feedback/ToastProvider';
import { useAuthStore } from '@/features/auth/store/authStore';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { marketplaceService } from '@/services/marketplaceService';
const gridVariants = {
visible: { transition: { staggerChildren: 0.06, delayChildren: 0.04 } },
};
const cardVariants = {
hidden: { opacity: 0, y: 16, scale: 0.97 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { duration: 0.35, ease: [0.33, 1, 0.68, 1] as const },
},
};
const WISHLIST_QUERY_KEY = ['wishlist'];
export const WishlistView: React.FC = () => {
const addToCart = useCartStore((state) => state.addItem);
const { addToast } = useToast();
const queryClient = useQueryClient();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const [playingPreview, setPlayingPreview] = useState<string | null>(null);
const {
data: wishlist = [],
isLoading,
isError,
error,
} = useQuery({
queryKey: WISHLIST_QUERY_KEY,
queryFn: () => marketplaceService.getWishlist(),
enabled: isAuthenticated,
});
const removeMutation = useMutation({
mutationFn: (productId: string) => marketplaceService.removeFromWishlist(productId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: WISHLIST_QUERY_KEY });
addToast('Removed from wishlist', 'info');
},
onError: (err: Error) => {
addToast(err.message || 'Failed to remove from wishlist', 'error');
},
});
const handleRemove = (id: string) => {
if (!isAuthenticated) return;
removeMutation.mutate(id);
};
const handleAddToCart = (product: Product) => {
addToCart(product);
handleRemove(product.id);
};
const handleAddAll = () => {
wishlist.forEach((p) => addToCart(p));
wishlist.forEach((p) => handleRemove(p.id));
addToast('All items moved to cart', 'success');
};
if (!isAuthenticated) {
return (
<EmptyState
variant="centered"
icon={<Heart className="w-full h-full" />}
title="Sign in to view your wishlist"
description="Create an account or log in to save items you love."
action={{
label: 'Sign in',
onClick: () => (window.location.href = '/login'),
}}
size="lg"
className="min-h-96"
/>
);
}
if (isError) {
return (
<EmptyState
variant="centered"
icon={<Heart className="w-full h-full" />}
title="Could not load wishlist"
description={error instanceof Error ? error.message : 'Something went wrong.'}
action={{
label: 'Try again',
onClick: () => queryClient.invalidateQueries({ queryKey: WISHLIST_QUERY_KEY }),
}}
size="lg"
className="min-h-96"
/>
);
}
if (isLoading) {
return (
<div className="max-w-6xl mx-auto pb-20">
<div className="flex flex-col md:flex-row justify-between items-end border-b border-border/50 pb-6 gap-4 mb-8">
<div>
<Skeleton className="h-9 w-48 mb-2" />
<Skeleton variant="text" className="w-32" />
</div>
<Skeleton className="h-10 w-44" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-96 w-full rounded-lg" />
))}
</div>
</div>
);
}
if (wishlist.length === 0) {
return (
<EmptyState
variant="centered"
icon={<Heart className="w-full h-full" />}
title="Your wishlist is empty"
description="Explore the marketplace and save items you love."
action={{
label: 'Browse Marketplace',
onClick: () => (window.location.href = '/marketplace'),
}}
size="lg"
className="min-h-96"
/>
);
}
return (
<div className="animate-fadeIn max-w-6xl mx-auto pb-20">
<div className="flex flex-col md:flex-row justify-between items-end border-b border-border/50 pb-6 gap-4 mb-8">
<div>
<h1 className="text-3xl font-heading font-bold text-foreground mb-2 tracking-tight">
WISHLIST
</h1>
<p className="text-muted-foreground font-mono text-sm">
{wishlist.length} saved items
</p>
</div>
<Button
variant="primary"
icon={<ShoppingCart className="w-4 h-4" />}
onClick={handleAddAll}
>
ADD ALL TO CART
</Button>
</div>
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
variants={gridVariants}
initial="hidden"
animate="visible"
>
{wishlist.map((product) => (
<motion.div key={product.id} variants={cardVariants}>
<Card
variant="default"
className="p-4 group hover:border-border/50 hover:shadow-lg transition-all duration-[var(--sumi-duration-normal)]"
>
<div className="flex gap-4">
<div className="relative w-24 h-24 bg-muted rounded-lg overflow-hidden flex-shrink-0">
<img
src={product.coverUrl}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-[var(--sumi-duration-slow)]"
/>
<div
className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer"
onClick={() =>
setPlayingPreview(
playingPreview === product.id ? null : product.id,
)
}
>
{playingPreview === product.id ? (
<Pause className="w-8 h-8 text-foreground" />
) : (
<Play className="w-8 h-8 text-foreground fill-current" />
)}
2026-01-07 18:39:21 +00:00
</div>
{product.isHot && (
<div className="absolute top-1 left-1 bg-warning text-warning-foreground text-xs font-bold px-1.5 py-0.5 rounded">
<Zap className="w-2 h-2 inline" /> HOT
</div>
)}
</div>
<div className="flex-1 min-w-0 flex flex-col justify-between">
2026-01-07 18:39:21 +00:00
<div>
<h3 className="font-bold text-foreground truncate">
{product.title}
</h3>
<p className="text-xs text-muted-foreground truncate">
{product.author}
</p>
<p className="text-xs text-muted-foreground mt-1 capitalize">
{product.type}
</p>
2026-01-07 18:39:21 +00:00
</div>
<div className="text-lg font-mono font-bold text-muted-foreground">
${product.price}
</div>
</div>
</div>
<div className="flex gap-2 mt-4 pt-4 border-t border-border/30">
<Button
variant="secondary"
size="sm"
className="flex-1"
onClick={() => handleAddToCart(product)}
>
Add to Cart
</Button>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-destructive"
onClick={() => handleRemove(product.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
2026-01-07 18:39:21 +00:00
</div>
</Card>
</motion.div>
))}
</motion.div>
</div>
);
};