PokemonCards
import React, { useMemo, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel } from "@/components/ui/dropdown-menu"; import { Handshake, ShoppingCart, Search, Plus, Star, Bell, Filter, SlidersHorizontal, Coins, Layers, ChevronRight, Sparkles, ShieldCheck, CheckCircle2, X, } from "lucide-react";
// --- Mock Data --- const MOCK_CARDS = [ { id: "base1-4", name: "Charizard", set: "Base Set", rarity: "Holo Rare", condition: "NM", price: 380, tradeOnly: false, image: "https://images.pokemontcg.io/base1/4_hires.png", owner: { name: "KantoCards", rating: 4.9, avatar: "https://api.dicebear.com/7.x/thumbs/svg?seed=Kanto", location: "Lyon, FR", }, wants: ["Blastoise Base Holo", "Lugia Neo Genesis"], }, { id: "sv3pt5-201", name: "Mew ex", set: "151", rarity: "Ultra Rare", condition: "LP", price: 75, tradeOnly: false, image: "https://images.pokemontcg.io/sv3pt5/151_hires.png", owner: { name: "JohtoShop", rating: 4.7, avatar: "https://api.dicebear.com/7.x/thumbs/svg?seed=Johto", location: "Paris, FR", }, wants: ["Mewtwo ex 151", "Arcanine ex Scarlet"], }, { id: "neo1-9", name: "Lugia", set: "Neo Genesis", rarity: "Holo Rare", condition: "EX", price: 520, tradeOnly: true, image: "https://images.pokemontcg.io/neo1/9_hires.png", owner: { name: "SilverWing", rating: 5.0, avatar: "https://api.dicebear.com/7.x/thumbs/svg?seed=Silver", location: "Nice, FR", }, wants: ["Charizard Base Holo", "Espeon Neo Discovery"], }, { id: "swsh12-186", name: "Lugia V (Alt Art)", set: "Silver Tempest", rarity: "Alternate Art", condition: "NM", price: 230, tradeOnly: false, image: "https://images.pokemontcg.io/swsh12/186_hires.png", owner: { name: "TempestTCG", rating: 4.6, avatar: "https://api.dicebear.com/7.x/thumbs/svg?seed=Tempest", location: "Bordeaux, FR", }, wants: ["Umbreon VMAX Alt Art", "Rayquaza Gold"], }, ];
const RARITIES = ["Common","Uncommon","Rare","Holo Rare","Ultra Rare","Alternate Art","Secret Rare"]; const CONDITIONS = ["M","NM","EX","LP","HP","DMG"];
// --- Utility Components --- function Rating({ value }:{ value:number }){ return ( <div className="flex items-center gap-1"> <Star className="w-4 h-4" /> <span className="text-sm font-medium">{value.toFixed(1)} ); }
function Price({ price }:{ price:number }){ return ( <div className="flex items-center gap-2"> <Coins className="w-4 h-4" /> <span className="font-semibold">{price.toLocaleString("fr-FR", { style: "currency", currency: "EUR" })} ); }
function CardImage({ src, alt }:{ src:string; alt:string }){ return ( <div className="relative aspect-[3/4] w-full overflow-hidden rounded-xl bg-gradient-to-br from-slate-100 to-slate-200"> {/* eslint-disable-next-line @next/next/no-img-element */} <img src={src} alt={alt} className="absolute inset-0 h-full w-full object-contain" /> <div className="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-black/5" /> ); }
// --- Listing Card --- function ListingCard({ item, onBuy, onProposeTrade }:{ item: any; onBuy: (i:any)=>void; onProposeTrade: (i:any)=>void; }){ return ( <Card className="group overflow-hidden rounded-2xl shadow-sm hover:shadow-md transition-shadow"> <CardContent className="p-4"> <div className="grid grid-cols-5 gap-4"> <div className="col-span-2"> <CardImage src={item.image} alt={item.name} /> <div className="col-span-3 flex flex-col"> <div className="flex items-start justify-between">
<div className="mt-3 flex items-center gap-3">
<Avatar className="h-8 w-8">
<AvatarImage src={item.owner.avatar} />
<AvatarFallback>{item.owner.name[0]}</AvatarFallback>
</Avatar>
<div className="text-sm">
<div className="font-medium">{item.owner.name}</div>
<div className="text-muted-foreground">{item.owner.location}</div>
</div>
<div className="ml-auto"><Rating value={item.owner.rating} /></div>
</div>
{!item.tradeOnly && (
<div className="mt-4"><Price price={item.price} /></div>
)}
{item.wants?.length ? (
<div className="mt-3">
<div className="text-xs uppercase tracking-wide text-muted-foreground mb-1">Recherche</div>
<div className="flex flex-wrap gap-2">
{item.wants.map((w:string) => (
<Badge key={w} variant="outline" className="rounded-full">{w}</Badge>
))}
</div>
</div>
) : null}
<div className="mt-4 flex gap-2">
<Button size="sm" className="rounded-xl" onClick={() => onProposeTrade(item)}>
<Handshake className="mr-2 h-4 w-4" /> Proposer un échange
</Button>
{!item.tradeOnly && (
<Button size="sm" variant="secondary" className="rounded-xl" onClick={() => onBuy(item)}>
<ShoppingCart className="mr-2 h-4 w-4" /> Acheter
</Button>
)}
</div>
</div>
</div>
</CardContent>
</Card>
); }
// --- Filters Bar --- function FiltersBar({ q, setQ, rarity, setRarity, condition, setCondition, tradeOnly, setTradeOnly }:{ q:string; setQ:(v:string)=>void; rarity:string|null; setRarity:(v:string|null)=>void; condition:string|null; setCondition:(v:string|null)=>void; tradeOnly:boolean; setTradeOnly:(v:boolean)=>void; }){ return ( <div className="flex flex-wrap items-center gap-2 rounded-2xl border p-3"> <div className="relative grow"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4" /> <Input value={q} onChange={(e)=>setQ(e.target.value)} placeholder="Rechercher une carte, une extension…" className="pl-9 rounded-xl" /> <div className="flex items-center gap-2"> <Select value={rarity ?? ""} onValueChange={(v)=>setRarity(v || null)}> <SelectTrigger className="w-[160px] rounded-xl"> <SelectValue placeholder="Rareté" /> </SelectTrigger> <SelectContent> {RARITIES.map(r => (<SelectItem key={r} value={r}>{r}</SelectItem>))} </SelectContent> </Select> <Select value={condition ?? ""} onValueChange={(v)=>setCondition(v || null)}> <SelectTrigger className="w-[140px] rounded-xl"> <SelectValue placeholder="État" /> </SelectTrigger> <SelectContent> {CONDITIONS.map(c => (<SelectItem key={c} value={c}>{c}</SelectItem>))} </SelectContent> </Select> <Button variant="outline" className="rounded-xl" onClick={()=>{setRarity(null); setCondition(null); setQ(""); setTradeOnly(false);}}> <SlidersHorizontal className="mr-2 h-4 w-4" /> Réinitialiser </Button> <div className="flex items-center gap-2 border rounded-xl px-3 py-2"> <Checkbox id="tradeonly" checked={tradeOnly} onCheckedChange={(v)=>setTradeOnly(Boolean(v))} /> <Label htmlFor="tradeonly" className="cursor-pointer">Échange uniquement</Label> ); }
// --- Create Listing Form ---
function CreateListingForm({ onCreate }:{ onCreate:(item:any)=>void }){
const [name, setName] = useState("");
const [setName, setSetName] = useState("");
const [rarity, setRarity] = useState
return ( <Card className="rounded-2xl"> <CardHeader> <CardTitle>Nouvelle annonce</CardTitle> <CardDescription>Publie une carte à vendre ou à échanger.</CardDescription> </CardHeader> <CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="md:col-span-1"> <CardImage src={image} alt={name || "Aperçu carte"} /> <div className="mt-2"> <Label className="text-sm">URL de l'image</Label> <Input value={image} onChange={(e)=>setImage(e.target.value)} placeholder="https://…" className="rounded-xl" /> <div className="md:col-span-2 grid grid-cols-2 gap-4"> <div className="col-span-2"> <Label>Nom</Label> <Input value={name} onChange={(e)=>setName(e.target.value)} placeholder="Charizard…" className="rounded-xl" />
${name}-${Date.now()},
name,
set: setName,
rarity,
condition,
price: tradeOnly ? undefined : Number(price || 0),
tradeOnly,
image,
owner: { name: "Toi", rating: 5, avatar: "https://api.dicebear.com/7.x/thumbs/svg?seed=You", location: "Évian-les-Bains, FR" },
wants: wants.split(",").map(s=>s.trim()).filter(Boolean),
});
setName(""); setSetName(""); setRarity(""); setCondition("NM"); setPrice(""); setTradeOnly(false); setWants("");
}}>
<Plus className="mr-2 h-4 w-4" /> Publier l'annonce
</Button>
</CardContent>
</Card>
);
}
// --- Trade Proposal Modal --- function TradeDialog({ open, onOpenChange, item, onConfirm }:{ open:boolean; onOpenChange:(v:boolean)=>void; item:any; onConfirm:(offer:any)=>void }){ const [message, setMessage] = useState("Salut ! Je te propose un échange."); const [addCash, setAddCash] = useState(0); const [myCard, setMyCard] = useState("Mewtwo ex 151");
return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="rounded-2xl sm:max-w-[560px]"> <DialogHeader> <DialogTitle>Proposer un échange pour {item?.name}</DialogTitle> </DialogHeader> <div className="space-y-4"> <div className="text-sm text-muted-foreground">Le vendeur recherche : {item?.wants?.join(", ") || "—"} <div className="grid grid-cols-2 gap-4">
// --- Buy (Checkout) Modal --- function CheckoutDialog({ open, onOpenChange, item }:{ open:boolean; onOpenChange:(v:boolean)=>void; item:any }){ return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="rounded-2xl"> <DialogHeader> <DialogTitle>Acheter {item?.name}</DialogTitle> </DialogHeader> <div className="grid grid-cols-3 gap-4"> <div className="col-span-1"> {item && <CardImage src={item.image} alt={item.name} />} <div className="col-span-2 space-y-2"> <div className="text-sm text-muted-foreground">Vendu par {item?.owner?.name} • {item?.owner?.location} <Price price={item?.price || 0} /> <div className="text-xs text-muted-foreground">Paiement sécurisé • Protection acheteur <ShieldCheck className="inline-block w-4 h-4" /> <DialogFooter> <Button className="rounded-xl"> <ShoppingCart className="mr-2 h-4 w-4" /> Payer maintenant </Button> </DialogFooter> </DialogContent> </Dialog> ); }
// --- Collection (simple mock) --- function CollectionView({ items }:{ items:any[] }){ const total = items.filter(i=>!i.tradeOnly).reduce((s,i)=> s + (i.price || 0), 0); return ( <div className="grid gap-4"> <div className="flex items-center justify-between"> <div className="text-sm text-muted-foreground">Valeur estimée (annonces non échange-only) <div className="text-lg font-semibold">{total.toLocaleString("fr-FR", { style:"currency", currency:"EUR" })} <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {items.map(i => ( <Card key={i.id} className="rounded-2xl"> <CardContent className="p-4 flex gap-4"> <div className="w-28"><CardImage src={i.image} alt={i.name} /> <div className="flex-1"> <div className="font-medium">{i.name} <div className="text-sm text-muted-foreground">{i.set} • {i.rarity} • {i.condition} <div className="mt-2 flex items-center gap-2"> {i.tradeOnly ? <Badge variant="secondary" className="rounded-full">Échange</Badge> : <Price price={i.price || 0} />} </CardContent> </Card> ))} ); }
// --- Alerts (watchlist mock) --- function AlertsPanel(){ return ( <div className="grid gap-3"> {["Umbreon VMAX Alt Art < 350€","Charizard Base Holo LP < 450€","Mewtwo ex 151 < 60€"].map((a,i)=>( <Card key={i} className="rounded-2xl"> <CardContent className="p-4 flex items-center justify-between"> <div className="flex items-center gap-2"><Bell className="h-4 w-4" />{a} <Badge className="rounded-full" variant="secondary">Actif</Badge> </CardContent> </Card> ))} ); }
// --- Exchange Matches (simple heuristic) --- function ExchangeMatches({ items, myNeeds }:{ items:any[]; myNeeds:string[] }){ const matches = useMemo(()=>{ return items.filter(i => i.wants?.some((w:string)=> myNeeds.some(n => w.toLowerCase().includes(n.toLowerCase())))); }, [items, myNeeds]);
return ( <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {matches.length === 0 && ( <Card className="rounded-2xl"> <CardContent className="p-6 text-center text-muted-foreground">Aucun match direct pour le moment. Ajoute des cartes dans "Mes recherches".</CardContent> </Card> )} {matches.map(item => ( <ListingCard key={item.id} item={item} onBuy={()=>{}} onProposeTrade={()=>{}} /> ))} ); }
// --- Main App --- export default function App(){ const [items, setItems] = useState(MOCK_CARDS); const [q, setQ] = useState(""); const [rarity, setRarity] = useState<string|null>(null); const [condition, setCondition] = useState<string|null>(null); const [tradeOnly, setTradeOnly] = useState(false);
const [buyOpen, setBuyOpen] = useState(false);
const [tradeOpen, setTradeOpen] = useState(false);
const [activeItem, setActiveItem] = useState
const myNeeds = ["Lugia", "Umbreon VMAX", "Mewtwo ex 151"]; // mock watchlist
const filtered = useMemo(()=>{ return items.filter(i => { const matchQ = q ? (i.name+" "+i.set+" "+i.rarity).toLowerCase().includes(q.toLowerCase()) : true; const matchR = rarity ? i.rarity === rarity : true; const matchC = condition ? i.condition === condition : true; const matchT = tradeOnly ? i.tradeOnly : true; return matchQ && matchR && matchC && matchT; }); }, [items, q, rarity, condition, tradeOnly]);
function handleBuy(it:any){ setActiveItem(it); setBuyOpen(true); } function handleTrade(it:any){ setActiveItem(it); setTradeOpen(true); }
return ( <div className="min-h-screen bg-gradient-to-b from-white to-slate-50"> <header className="sticky top-0 z-30 backdrop-blur bg-white/70 border-b"> <div className="max-w-6xl mx-auto px-4 py-3 flex items-center gap-3"> <div className="flex items-center gap-2"> <Layers className="h-6 w-6" /> <span className="font-semibold">PokéTrade <Badge className="ml-2 rounded-full" variant="secondary">Prototype</Badge> <div className="ml-auto flex items-center gap-3"> <Button variant="outline" className="rounded-xl"><Sparkles className="mr-2 h-4 w-4" /> Booster</Button> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="rounded-xl"> <Avatar className="h-6 w-6 mr-2"> <AvatarImage src="https://api.dicebear.com/7.x/thumbs/svg?seed=Nico" /> <AvatarFallback>N</AvatarFallback> </Avatar> Nicolas </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="rounded-2xl"> <DropdownMenuLabel>Mon compte</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem>Profil</DropdownMenuItem> <DropdownMenuItem>Paiements</DropdownMenuItem> <DropdownMenuItem>Déconnexion</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu>
<main className="max-w-6xl mx-auto px-4 py-6">
<div className="mb-6">
<h1 className="text-2xl font-bold tracking-tight">Marketplace & Échanges</h1>
<p className="text-muted-foreground">Achète, vends et échange des cartes Pokémon en toute sécurité.</p>
</div>
<Tabs defaultValue="market">
<TabsList className="rounded-2xl">
<TabsTrigger value="market" className="rounded-xl"><ShoppingCart className="mr-2 h-4 w-4" /> Marketplace</TabsTrigger>
<TabsTrigger value="exchange" className="rounded-xl"><Handshake className="mr-2 h-4 w-4" /> Échanges</TabsTrigger>
<TabsTrigger value="sell" className="rounded-xl"><Plus className="mr-2 h-4 w-4" /> Publier</TabsTrigger>
<TabsTrigger value="collection" className="rounded-xl"><Layers className="mr-2 h-4 w-4" /> Ma collection</TabsTrigger>
<TabsTrigger value="alerts" className="rounded-xl"><Bell className="mr-2 h-4 w-4" /> Alertes</TabsTrigger>
</TabsList>
{/* Marketplace */}
<TabsContent value="market" className="mt-6">
<div className="space-y-4">
<FiltersBar q={q} setQ={setQ} rarity={rarity} setRarity={setRarity} condition={condition} setCondition={setCondition} tradeOnly={tradeOnly} setTradeOnly={setTradeOnly} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filtered.map(item => (
<motion.div key={item.id} layout initial={{opacity:0, y:8}} animate={{opacity:1, y:0}}>
<ListingCard item={item} onBuy={handleBuy} onProposeTrade={handleTrade} />
</motion.div>
))}
</div>
{filtered.length === 0 && (
<Card className="rounded-2xl">
<CardContent className="p-8 text-center text-muted-foreground">
<Filter className="mx-auto mb-2 h-6 w-6" /> Aucun résultat. Modifie les filtres.
</CardContent>
</Card>
)}
</div>
</TabsContent>
{/* Échanges */}
<TabsContent value="exchange" className="mt-6">
<div className="mb-4">
<div className="text-sm text-muted-foreground mb-2">Mes recherches prioritaires</div>
<div className="flex flex-wrap gap-2">
{myNeeds.map(n => (<Badge key={n} className="rounded-full" variant="secondary">{n}</Badge>))}
</div>
</div>
<ExchangeMatches items={items} myNeeds={myNeeds} />
</TabsContent>
{/* Publier */}
<TabsContent value="sell" className="mt-6">
<CreateListingForm onCreate={(it)=> setItems(prev => [it, ...prev])} />
</TabsContent>
{/* Collection */}
<TabsContent value="collection" className="mt-6">
<CollectionView items={items.slice(0,4)} />
</TabsContent>
{/* Alertes */}
<TabsContent value="alerts" className="mt-6">
<AlertsPanel />
</TabsContent>
</Tabs>
</main>
<AnimatePresence>
{activeItem && (
<>
<CheckoutDialog open={buyOpen} onOpenChange={setBuyOpen} item={activeItem} />
<TradeDialog open={tradeOpen} onOpenChange={setTradeOpen} item={activeItem} onConfirm={(offer)=>{
// For prototype we just show a toast-like alert
alert(`Proposition envoyée: ${offer.myCard} + ${offer.addCash}€\n\nMessage:\n${offer.message}`);
}} />
</>
)}
</AnimatePresence>
<footer className="max-w-6xl mx-auto px-4 py-10 text-center text-xs text-muted-foreground">
<div className="flex items-center justify-center gap-2">
<CheckCircle2 className="h-4 w-4" /> Prototype UI — Paiement, messagerie, et vérification des utilisateurs à connecter à un backend.
</div>
</footer>
</div>
); }