Files
football-next/app/(user)/golden-cards/GoldenCardsClient.tsx
2026-05-03 17:01:46 +03:30

291 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}