io
This commit is contained in:
290
app/(user)/golden-cards/GoldenCardsClient.tsx
Normal file
290
app/(user)/golden-cards/GoldenCardsClient.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
20
app/(user)/golden-cards/page.tsx
Normal file
20
app/(user)/golden-cards/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user