291 lines
11 KiB
TypeScript
291 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import Image from "next/image";
|
||
|
||
type Player = {
|
||
id: string;
|
||
name: string;
|
||
image: string | null;
|
||
position: "GK" | "DEF" | "MID" | "FWD";
|
||
price: number;
|
||
country: { name: string; flagUrl: string | null };
|
||
};
|
||
|
||
type Card = {
|
||
id: string;
|
||
status: "SEALED" | "OPENED";
|
||
state: "IN_INVENTORY" | "IN_TEAM" | "SOLD";
|
||
acquiredDate: string;
|
||
openedAt: string | null;
|
||
player: Player;
|
||
};
|
||
|
||
const POSITION_LABELS: Record<Player["position"], string> = {
|
||
GK: "دروازهبان",
|
||
DEF: "مدافع",
|
||
MID: "هافبک",
|
||
FWD: "مهاجم",
|
||
};
|
||
|
||
function saleValue(price: number) {
|
||
return Math.round(price * 0.7);
|
||
}
|
||
|
||
function SealedCard({ card, onReveal }: { card: Card; onReveal: (id: string) => void }) {
|
||
const [opening, setOpening] = useState(false);
|
||
|
||
async function handleOpen() {
|
||
setOpening(true);
|
||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||
onReveal(card.id);
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={`relative rounded-2xl border border-yellow-400/30 bg-gradient-to-br from-yellow-900/30 to-amber-900/20 backdrop-blur p-6 flex flex-col items-center gap-4 cursor-pointer hover:border-yellow-400/60 transition-all duration-300 ${opening ? "scale-95 opacity-70" : "hover:scale-105"}`}
|
||
onClick={!opening ? handleOpen : undefined}
|
||
>
|
||
<div className="absolute inset-0 rounded-2xl bg-yellow-400/5 blur-xl" />
|
||
<div className="relative z-10 flex flex-col items-center gap-3">
|
||
<div className={`w-20 h-20 rounded-2xl bg-gradient-to-br from-yellow-400 to-amber-600 flex items-center justify-center text-4xl shadow-lg shadow-yellow-500/30 ${opening ? "animate-spin" : "animate-pulse"}`}>
|
||
🎴
|
||
</div>
|
||
<p className="font-bold text-yellow-300">کارت ویژه مهر شده</p>
|
||
<p className="text-xs text-gray-400">دریافت: {new Date(card.acquiredDate).toLocaleDateString("fa-IR")}</p>
|
||
<button className="mt-1 px-5 py-2 rounded-xl bg-gradient-to-r from-yellow-500 to-amber-500 text-black font-bold text-sm hover:opacity-90 transition shadow-lg shadow-yellow-500/20">
|
||
{opening ? "در حال باز شدن..." : "باز کردن کارت"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function OpenedCard({
|
||
card,
|
||
loading,
|
||
onAdd,
|
||
onSell,
|
||
}: {
|
||
card: Card;
|
||
loading: boolean;
|
||
onAdd?: () => void;
|
||
onSell?: () => void;
|
||
}) {
|
||
return (
|
||
<div className="relative rounded-2xl border border-white/10 bg-white/5 backdrop-blur overflow-hidden">
|
||
<div className="relative z-10 p-5 flex flex-col items-center gap-3">
|
||
<div className="relative w-20 h-20 rounded-full overflow-hidden bg-gray-900 border-2 border-amber-400/50">
|
||
{card.player.image ? (
|
||
<Image
|
||
src={`/uploads/players/${card.player.image}`}
|
||
alt={card.player.name}
|
||
width={80}
|
||
height={80}
|
||
className="object-cover w-full h-full"
|
||
/>
|
||
) : (
|
||
<div className="w-full h-full flex items-center justify-center text-3xl">⚽</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="text-center">
|
||
<p className="font-bold text-white">{card.player.name}</p>
|
||
<p className="text-xs text-gray-400">{card.player.country.name}</p>
|
||
</div>
|
||
|
||
<span className="text-xs font-bold px-3 py-1 rounded-full bg-amber-500 text-white">
|
||
{POSITION_LABELS[card.player.position]}
|
||
</span>
|
||
|
||
<div className="text-xs text-amber-300">
|
||
{card.state === "IN_TEAM" ? "در تیم" : `فروش: ${saleValue(card.player.price)}M`}
|
||
</div>
|
||
|
||
<div className="w-full flex gap-2 pt-2">
|
||
{card.state === "IN_INVENTORY" && onAdd && (
|
||
<button
|
||
onClick={onAdd}
|
||
disabled={loading}
|
||
className="flex-1 rounded-xl bg-green-700 py-2 text-sm font-bold text-white hover:bg-green-800 disabled:opacity-50 transition"
|
||
>
|
||
افزودن به تیم
|
||
</button>
|
||
)}
|
||
{card.state !== "SOLD" && onSell && (
|
||
<button
|
||
onClick={onSell}
|
||
disabled={loading}
|
||
className="flex-1 rounded-xl bg-amber-500 py-2 text-sm font-bold text-white hover:bg-amber-600 disabled:opacity-50 transition"
|
||
>
|
||
فروش
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function GoldenCardsClient({ initialCards }: { initialCards: Card[] }) {
|
||
const [cards, setCards] = useState<Card[]>(initialCards);
|
||
const [revealedCard, setRevealedCard] = useState<Card | null>(null);
|
||
const [loadingId, setLoadingId] = useState<string | null>(null);
|
||
const [replacementDialog, setReplacementDialog] = useState<{
|
||
card: Card;
|
||
candidates: Array<{ playerId: string; name: string; isBench: boolean; isSpecial: boolean }>;
|
||
} | null>(null);
|
||
|
||
async function handleReveal(cardId: string) {
|
||
const res = await fetch(`/api/golden-cards/${cardId}/reveal`, { method: "POST" });
|
||
if (res.ok) {
|
||
const updated: Card = await res.json();
|
||
setCards((prev) => prev.map((card) => (card.id === cardId ? updated : card)));
|
||
setRevealedCard(updated);
|
||
}
|
||
}
|
||
|
||
async function handleSell(cardId: string) {
|
||
setLoadingId(cardId);
|
||
const res = await fetch(`/api/golden-cards/${cardId}/sell`, { method: "POST" });
|
||
if (res.ok) {
|
||
setCards((prev) => prev.map((card) => (card.id === cardId ? { ...card, state: "SOLD" } : card)));
|
||
}
|
||
setLoadingId(null);
|
||
}
|
||
|
||
async function handleAdd(cardId: string, replacePlayerId?: string) {
|
||
setLoadingId(cardId);
|
||
const res = await fetch(`/api/golden-cards/${cardId}/add-to-team`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(replacePlayerId ? { replacePlayerId } : {}),
|
||
});
|
||
const data = await res.json().catch(() => null);
|
||
if (res.ok) {
|
||
setCards((prev) =>
|
||
prev.map((card) => {
|
||
if (card.id === cardId) return { ...card, state: "IN_TEAM" };
|
||
if (data?.replacedGoldenCardId && card.id === data.replacedGoldenCardId) return { ...card, state: "IN_INVENTORY" };
|
||
return card;
|
||
})
|
||
);
|
||
setReplacementDialog(null);
|
||
} else if (res.status === 409 && data?.needsReplacement) {
|
||
const card = cards.find((item) => item.id === cardId);
|
||
if (card) {
|
||
setReplacementDialog({ card, candidates: data.candidates });
|
||
}
|
||
}
|
||
setLoadingId(null);
|
||
}
|
||
|
||
const sealed = cards.filter((card) => card.status === "SEALED");
|
||
const opened = cards.filter((card) => card.status === "OPENED" && card.state !== "SOLD");
|
||
const sold = cards.filter((card) => card.state === "SOLD");
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto">
|
||
{replacementDialog && (
|
||
<div className="fixed inset-0 z-50 bg-black/70 p-4 flex items-center justify-center">
|
||
<div className="w-full max-w-md rounded-3xl bg-white p-6 shadow-2xl">
|
||
<h3 className="text-xl font-bold mb-2">پست بازیکن پر است</h3>
|
||
<p className="text-sm text-gray-600 mb-4">
|
||
برای اضافه کردن {replacementDialog.card.player.name} یکی از بازیکنان این پست را انتخاب کنید.
|
||
</p>
|
||
<div className="space-y-2">
|
||
{replacementDialog.candidates.map((candidate) => (
|
||
<button
|
||
key={candidate.playerId}
|
||
onClick={() => handleAdd(replacementDialog.card.id, candidate.playerId)}
|
||
className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right hover:border-green-500 hover:bg-green-50 transition"
|
||
>
|
||
<div className="font-bold text-black">{candidate.name}</div>
|
||
<div className="text-xs text-gray-500">
|
||
{candidate.isBench ? "ذخیره" : "فیکس"}
|
||
{candidate.isSpecial ? " | کارت ویژه" : ""}
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button onClick={() => setReplacementDialog(null)} className="w-full mt-4 rounded-2xl bg-gray-100 py-3 text-sm font-bold text-gray-700">
|
||
بستن
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-center mb-10">
|
||
<h1 className="text-3xl font-black mb-2 text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-amber-500">
|
||
کارت ویژه
|
||
</h1>
|
||
<p className="text-gray-400">بازیکنان ویژه را به تیم اضافه کنید یا با فروش آنها بودجه بگیرید.</p>
|
||
</div>
|
||
|
||
{revealedCard && (
|
||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4" onClick={() => setRevealedCard(null)}>
|
||
<div className="bg-gray-900 border border-yellow-400/30 rounded-3xl p-8 max-w-sm w-full text-center" onClick={(e) => e.stopPropagation()}>
|
||
<p className="text-yellow-400 font-bold mb-4 text-lg">کارت شما باز شد</p>
|
||
<OpenedCard
|
||
card={revealedCard}
|
||
loading={loadingId === revealedCard.id}
|
||
onAdd={revealedCard.state === "IN_INVENTORY" ? () => handleAdd(revealedCard.id) : undefined}
|
||
onSell={() => handleSell(revealedCard.id)}
|
||
/>
|
||
<button onClick={() => setRevealedCard(null)} className="mt-6 px-6 py-2 rounded-xl bg-white/10 hover:bg-white/20 transition text-sm">
|
||
بستن
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{cards.length === 0 && (
|
||
<div className="text-center py-20 text-gray-500">
|
||
<div className="text-5xl mb-4">🎴</div>
|
||
<p>هنوز کارت ویژه ندارید</p>
|
||
<p className="text-sm mt-1">در کوییز روزانه شرکت کنید تا شانس دریافت کارت ویژه داشته باشید.</p>
|
||
</div>
|
||
)}
|
||
|
||
{sealed.length > 0 && (
|
||
<div className="mb-8">
|
||
<h2 className="font-bold text-lg mb-4 text-yellow-400">کارتهای مهر شده ({sealed.length})</h2>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||
{sealed.map((card) => (
|
||
<SealedCard key={card.id} card={card} onReveal={handleReveal} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{opened.length > 0 && (
|
||
<div className="mb-8">
|
||
<h2 className="font-bold text-lg mb-4 text-gray-300">کارتهای آماده استفاده ({opened.length})</h2>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||
{opened.map((card) => (
|
||
<OpenedCard
|
||
key={card.id}
|
||
card={card}
|
||
loading={loadingId === card.id}
|
||
onAdd={card.state === "IN_INVENTORY" ? () => handleAdd(card.id) : undefined}
|
||
onSell={() => handleSell(card.id)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{sold.length > 0 && (
|
||
<div>
|
||
<h2 className="font-bold text-lg mb-4 text-gray-500">فروختهشدهها ({sold.length})</h2>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 opacity-70">
|
||
{sold.map((card) => (
|
||
<OpenedCard key={card.id} card={card} loading={false} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|