This commit is contained in:
2026-05-03 17:01:46 +03:30
parent b5ad5420b2
commit 9c30295b4b
76 changed files with 7891 additions and 461 deletions

View File

@@ -0,0 +1,290 @@
"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>
);
}

View File

@@ -0,0 +1,20 @@
import { requireAuth } from "@/lib/session";
import { db } from "@/lib/db";
import GoldenCardsClient from "./GoldenCardsClient";
export default async function GoldenCardsPage() {
const session = await requireAuth();
const userId = (session.user as any).id;
const cards = await db.goldenCard.findMany({
where: { userId },
include: { player: { include: { country: true } } },
orderBy: { acquiredDate: "desc" },
});
return (
<div className="min-h-screen bg-gray-950 text-white py-10 px-4">
<GoldenCardsClient initialCards={cards as any} />
</div>
);
}