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 (
{value.toFixed(1)}
);
}
function Price({ price }:{ price:number }){
return (
{price.toLocaleString("fr-FR", { style: "currency", currency: "EUR" })}
);
}
function CardImage({ src, alt }:{ src:string; alt:string }){
return (
{/* eslint-disable-next-line @next/next/no-img-element */}
);
}
// --- Listing Card ---
function ListingCard({ item, onBuy, onProposeTrade }:{
item: any;
onBuy: (i:any)=>void;
onProposeTrade: (i:any)=>void;
}){
return (
{item.name}
{item.set} • {item.rarity} • État {item.condition}
<Badge variant={item.tradeOnly ? "secondary" : "default"} className="rounded-full">{item.tradeOnly ? "Échange" : "À vendre"}
<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 (
<Input value={q} onChange={(e)=>setQ(e.target.value)} placeholder="Rechercher une carte, une extension…" className="pl-9 rounded-xl" />
<Select value={rarity ?? ""} onValueChange={(v)=>setRarity(v || null)}>
{RARITIES.map(r => ({r}))}
<Select value={condition ?? ""} onValueChange={(v)=>setCondition(v || null)}>
{CONDITIONS.map(c => ({c}))}
<Button variant="outline" className="rounded-xl" onClick={()=>{setRarity(null); setCondition(null); setQ(""); setTradeOnly(false);}}>
Réinitialiser
<Checkbox id="tradeonly" checked={tradeOnly} onCheckedChange={(v)=>setTradeOnly(Boolean(v))} />
Échange uniquement
);
}
// --- Create Listing Form ---
function CreateListingForm({ onCreate }:{ onCreate:(item:any)=>void }){
const [name, setName] = useState("");
const [setName, setSetName] = useState("");
const [rarity, setRarity] = useState("");
const [condition, setCondition] = useState("NM");
const [price, setPrice] = useState("");
const [tradeOnly, setTradeOnly] = useState(false);
const [wants, setWants] = useState("");
const [image, setImage] = useState("https://images.pokemontcg.io/sv3pt5/006_hires.png");
return (
Nouvelle annonce
Publie une carte à vendre ou à échanger.
<CardImage src={image} alt={name || "Aperçu carte"} />
URL de l'image
<Input value={image} onChange={(e)=>setImage(e.target.value)} placeholder="https://…" className="rounded-xl" />
Nom
<Input value={name} onChange={(e)=>setName(e.target.value)} placeholder="Charizard…" className="rounded-xl" />
Extension
<Input value={setName} onChange={(e)=>setSetName(e.target.value)} placeholder="Base Set, 151…" className="rounded-xl" />
Rareté
{RARITIES.map(r => ({r}))}
État
{CONDITIONS.map(c => ({c}))}
Prix (€)
<Input type="number" value={price} onChange={(e)=>setPrice(e.target.value)} placeholder="ex. 120" className="rounded-xl" />
<Checkbox id="tradeOnlyNew" checked={tradeOnly} onCheckedChange={(v)=>setTradeOnly(Boolean(v))} />
Échange uniquement
Cartes recherchées (séparées par des virgules)
<Textarea value={wants} onChange={(e)=>setWants(e.target.value)} placeholder="Ex: Umbreon VMAX Alt Art, Lugia Neo…" className="rounded-xl" />
<Button className="rounded-xl" onClick={()=>{
if(!name) return;
onCreate({
id:
${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("");
}}>
Publier l'annonce
);
}
// --- 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 (
Proposer un échange pour {item?.name}
Le vendeur recherche : {item?.wants?.join(", ") || "—"}
Ta carte à offrir
<Input value={myCard} onChange={(e)=>setMyCard(e.target.value)} className="rounded-xl" />
+ Soulte (€) facultative
<Input type="number" value={addCash} onChange={(e)=>setAddCash(Number(e.target.value || 0))} className="rounded-xl" />
Message
<Textarea value={message} onChange={(e)=>setMessage(e.target.value)} className="rounded-xl" />
<Button className="rounded-xl" onClick={()=>{ onConfirm({ myCard, addCash, message }); onOpenChange(false); }}>
Envoyer la proposition
);
}
// --- Buy (Checkout) Modal ---
function CheckoutDialog({ open, onOpenChange, item }:{ open:boolean; onOpenChange:(v:boolean)=>void; item:any }){
return (
Acheter {item?.name}
{item && }
Vendu par {item?.owner?.name} • {item?.owner?.location}
<Price price={item?.price || 0} />
Paiement sécurisé • Protection acheteur
Payer maintenant
);
}
// --- Collection (simple mock) ---
function CollectionView({ items }:{ items:any[] }){
const total = items.filter(i=>!i.tradeOnly).reduce((s,i)=> s + (i.price || 0), 0);
return (
Valeur estimée (annonces non échange-only)
{total.toLocaleString("fr-FR", { style:"currency", currency:"EUR" })}
{items.map(i => (
{i.name}
{i.set} • {i.rarity} • {i.condition}
{i.tradeOnly ? Échange : <Price price={i.price || 0} />}
))}
);
}
// --- Alerts (watchlist mock) ---
function AlertsPanel(){
return (
{["Umbreon VMAX Alt Art < 350€","Charizard Base Holo LP < 450€","Mewtwo ex 151 < 60€"].map((a,i)=>(
{a}
Actif
))}
);
}
// --- 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 (
{matches.length === 0 && (
Aucun match direct pour le moment. Ajoute des cartes dans "Mes recherches".
)}
{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(null);
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 (
PokéTrade
Prototype
Booster
N
Nicolas
Mon compte
Profil
Paiements
Déconnexion
<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>
);
}
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 (
{value.toFixed(1)}
);
}
function Price({ price }:{ price:number }){
return (
{price.toLocaleString("fr-FR", { style: "currency", currency: "EUR" })}
);
}
function CardImage({ src, alt }:{ src:string; alt:string }){
return (
{/* eslint-disable-next-line @next/next/no-img-element */}
);
}
// --- Listing Card ---
function ListingCard({ item, onBuy, onProposeTrade }:{
item: any;
onBuy: (i:any)=>void;
onProposeTrade: (i:any)=>void;
}){
return (
{item.name}
{item.set} • {item.rarity} • État {item.condition}
<Badge variant={item.tradeOnly ? "secondary" : "default"} className="rounded-full">{item.tradeOnly ? "Échange" : "À vendre"}
);
}
// --- 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 (
<Input value={q} onChange={(e)=>setQ(e.target.value)} placeholder="Rechercher une carte, une extension…" className="pl-9 rounded-xl" />
<Select value={rarity ?? ""} onValueChange={(v)=>setRarity(v || null)}>
{RARITIES.map(r => ({r}))}
<Select value={condition ?? ""} onValueChange={(v)=>setCondition(v || null)}>
{CONDITIONS.map(c => ({c}))}
<Button variant="outline" className="rounded-xl" onClick={()=>{setRarity(null); setCondition(null); setQ(""); setTradeOnly(false);}}>
Réinitialiser
<Checkbox id="tradeonly" checked={tradeOnly} onCheckedChange={(v)=>setTradeOnly(Boolean(v))} />
Échange uniquement
);
}
// --- Create Listing Form ---
function CreateListingForm({ onCreate }:{ onCreate:(item:any)=>void }){
const [name, setName] = useState("");
const [setName, setSetName] = useState("");
const [rarity, setRarity] = useState("");
const [condition, setCondition] = useState("NM");
const [price, setPrice] = useState("");
const [tradeOnly, setTradeOnly] = useState(false);
const [wants, setWants] = useState("");
const [image, setImage] = useState("https://images.pokemontcg.io/sv3pt5/006_hires.png");
return (
Nouvelle annonce
Publie une carte à vendre ou à échanger.
<CardImage src={image} alt={name || "Aperçu carte"} />
URL de l'image
<Input value={image} onChange={(e)=>setImage(e.target.value)} placeholder="https://…" className="rounded-xl" />
Nom
<Input value={name} onChange={(e)=>setName(e.target.value)} placeholder="Charizard…" className="rounded-xl" />
Extension
<Input value={setName} onChange={(e)=>setSetName(e.target.value)} placeholder="Base Set, 151…" className="rounded-xl" />
Rareté
{RARITIES.map(r => ({r}))}
État
{CONDITIONS.map(c => ({c}))}
Prix (€)
<Input type="number" value={price} onChange={(e)=>setPrice(e.target.value)} placeholder="ex. 120" className="rounded-xl" />
<Checkbox id="tradeOnlyNew" checked={tradeOnly} onCheckedChange={(v)=>setTradeOnly(Boolean(v))} />
Échange uniquement
Cartes recherchées (séparées par des virgules)
<Textarea value={wants} onChange={(e)=>setWants(e.target.value)} placeholder="Ex: Umbreon VMAX Alt Art, Lugia Neo…" className="rounded-xl" />
<Button className="rounded-xl" onClick={()=>{
if(!name) return;
onCreate({
id:
${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("");
}}>
Publier l'annonce
);
}
// --- 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 (
Proposer un échange pour {item?.name}
Ta carte à offrir
<Input value={myCard} onChange={(e)=>setMyCard(e.target.value)} className="rounded-xl" />
+ Soulte (€) facultative
<Input type="number" value={addCash} onChange={(e)=>setAddCash(Number(e.target.value || 0))} className="rounded-xl" />
Message
<Textarea value={message} onChange={(e)=>setMessage(e.target.value)} className="rounded-xl" />
<Button className="rounded-xl" onClick={()=>{ onConfirm({ myCard, addCash, message }); onOpenChange(false); }}>
Envoyer la proposition
);
}
// --- Buy (Checkout) Modal ---
function CheckoutDialog({ open, onOpenChange, item }:{ open:boolean; onOpenChange:(v:boolean)=>void; item:any }){
return (
Acheter {item?.name}
{item && }
<Price price={item?.price || 0} />
Payer maintenant
);
}
// --- Collection (simple mock) ---
function CollectionView({ items }:{ items:any[] }){
const total = items.filter(i=>!i.tradeOnly).reduce((s,i)=> s + (i.price || 0), 0);
return (
{items.map(i => (
{i.tradeOnly ? Échange : <Price price={i.price || 0} />}
))}
);
}
// --- Alerts (watchlist mock) ---
function AlertsPanel(){
return (
{["Umbreon VMAX Alt Art < 350€","Charizard Base Holo LP < 450€","Mewtwo ex 151 < 60€"].map((a,i)=>(
Actif
))}
);
}
// --- 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 (
{matches.length === 0 && (
Aucun match direct pour le moment. Ajoute des cartes dans "Mes recherches".
)}
{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(null);
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 (
PokéTrade
Prototype
Booster
N
Nicolas
Mon compte
Profil
Paiements
Déconnexion
);
}