Files
football-next/app/(admin)/admin/countries/[id]/lineup/DefaultLineupEditor.tsx
2026-04-07 10:38:28 +03:30

795 lines
28 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 { useRouter } from "next/navigation";
import Image from "next/image";
import {
DndContext,
DragOverlay,
closestCenter,
PointerSensor,
useSensor,
useSensors,
DragStartEvent,
DragEndEvent,
useDraggable,
useDroppable,
} from "@dnd-kit/core";
import { arrayMove } from "@dnd-kit/sortable";
type Player = {
id: string;
name: string;
position: string;
price: number;
image: string | null;
};
type Country = {
id: string;
name: string;
code: string;
flagUrl: string | null;
defaultFormation: string;
defaultLineupPlayerIds: string[];
defaultCaptainId: string | null;
};
const FORMATIONS: Record<string, { label: string; def: number; mid: number; fwd: number }> = {
"4-3-3": { label: "۴-۳-۳", def: 4, mid: 3, fwd: 3 },
"4-4-2": { label: "۴-۴-۲", def: 4, mid: 4, fwd: 2 },
"4-5-1": { label: "۴-۵-۱", def: 4, mid: 5, fwd: 1 },
"3-5-2": { label: "۳-۵-۲", def: 3, mid: 5, fwd: 2 },
"3-4-3": { label: "۳-۴-۳", def: 3, mid: 4, fwd: 3 },
"5-3-2": { label: "۵-۳-۲", def: 5, mid: 3, fwd: 2 },
"5-4-1": { label: "۵-۴-۱", def: 5, mid: 4, fwd: 1 },
};
const POSITION_LABELS: Record<string, string> = {
GK: "دروازه‌بان",
DEF: "مدافع",
MID: "هافبک",
FWD: "مهاجم",
};
const POSITION_COLORS: Record<string, string> = {
GK: "bg-yellow-100 text-yellow-800",
DEF: "bg-blue-100 text-blue-800",
MID: "bg-green-100 text-green-800",
FWD: "bg-red-100 text-red-800",
};
export default function DefaultLineupEditor({ country, players }: { country: Country; players: Player[] }) {
const router = useRouter();
const [formation, setFormation] = useState(country.defaultFormation);
const [selectedPlayerIds, setSelectedPlayerIds] = useState<string[]>(country.defaultLineupPlayerIds);
const [captainId, setCaptainId] = useState<string | null>(country.defaultCaptainId);
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState<{ text: string; type: "error" | "success" } | null>(null);
const [activeId, setActiveId] = useState<string | null>(null);
const [pendingFormation, setPendingFormation] = useState<string | null>(null);
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
const [playersToRemove, setPlayersToRemove] = useState<{ position: string; count: number; players: Player[] }>({
position: "",
count: 0,
players: [],
});
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
const fmt = FORMATIONS[formation] ?? FORMATIONS["4-3-3"];
const gkPlayers = players.filter((p) => p.position === "GK");
const defPlayers = players.filter((p) => p.position === "DEF");
const midPlayers = players.filter((p) => p.position === "MID");
const fwdPlayers = players.filter((p) => p.position === "FWD");
const selectedGk = selectedPlayerIds.filter((id) => gkPlayers.find((p) => p.id === id));
const selectedDef = selectedPlayerIds.filter((id) => defPlayers.find((p) => p.id === id));
const selectedMid = selectedPlayerIds.filter((id) => midPlayers.find((p) => p.id === id));
const selectedFwd = selectedPlayerIds.filter((id) => fwdPlayers.find((p) => p.id === id));
function handleFormationChange(newFormation: string) {
const newFmt = FORMATIONS[newFormation];
const currentFmt = FORMATIONS[formation];
// بررسی اینکه آیا تعداد بازیکنان در هر پست کاهش پیدا می‌کنه
const defDiff = currentFmt.def - newFmt.def;
const midDiff = currentFmt.mid - newFmt.mid;
const fwdDiff = currentFmt.fwd - newFmt.fwd;
// اگر در هر پستی تعداد کاهش پیدا کرد و بازیکن اضافی داریم
if (defDiff > 0 && selectedDef.length > newFmt.def) {
setPendingFormation(newFormation);
setPlayersToRemove({
position: "DEF",
count: defDiff,
players: defPlayers.filter((p) => selectedDef.includes(p.id)),
});
setShowRemoveDialog(true);
return;
}
if (midDiff > 0 && selectedMid.length > newFmt.mid) {
setPendingFormation(newFormation);
setPlayersToRemove({
position: "MID",
count: midDiff,
players: midPlayers.filter((p) => selectedMid.includes(p.id)),
});
setShowRemoveDialog(true);
return;
}
if (fwdDiff > 0 && selectedFwd.length > newFmt.fwd) {
setPendingFormation(newFormation);
setPlayersToRemove({
position: "FWD",
count: fwdDiff,
players: fwdPlayers.filter((p) => selectedFwd.includes(p.id)),
});
setShowRemoveDialog(true);
return;
}
// اگر مشکلی نبود، فرمیشن رو تغییر بده
setFormation(newFormation);
}
function handleRemovePlayer(playerId: string) {
const newSelected = selectedPlayerIds.filter((id) => id !== playerId);
setSelectedPlayerIds(newSelected);
if (captainId === playerId) setCaptainId(null);
// بررسی کنیم آیا به اندازه کافی حذف شده
const newFmt = FORMATIONS[pendingFormation!];
const position = playersToRemove.position;
const currentCount = newSelected.filter((id) => {
const p = players.find((pl) => pl.id === id);
return p?.position === position;
}).length;
let maxCount = 0;
if (position === "DEF") maxCount = newFmt.def;
else if (position === "MID") maxCount = newFmt.mid;
else if (position === "FWD") maxCount = newFmt.fwd;
if (currentCount <= maxCount) {
// تعداد درست شد، فرمیشن رو تغییر بده
setFormation(pendingFormation!);
setShowRemoveDialog(false);
setPendingFormation(null);
setMsg({ text: "فرمیشن تغییر کرد", type: "success" });
setTimeout(() => setMsg(null), 3000);
}
}
function handleCancelFormationChange() {
setShowRemoveDialog(false);
setPendingFormation(null);
setPlayersToRemove({ position: "", count: 0, players: [] });
}
function addPlayer(playerId: string, position: string) {
if (selectedPlayerIds.includes(playerId)) return;
const posPlayers = selectedPlayerIds.filter((id) => {
const p = players.find((pl) => pl.id === id);
return p?.position === position;
});
let maxCount = 1;
if (position === "DEF") maxCount = fmt.def;
else if (position === "MID") maxCount = fmt.mid;
else if (position === "FWD") maxCount = fmt.fwd;
if (posPlayers.length >= maxCount) {
setMsg({ text: `حداکثر ${maxCount} ${position} می‌توانید انتخاب کنید`, type: "error" });
setTimeout(() => setMsg(null), 3000);
return;
}
setSelectedPlayerIds([...selectedPlayerIds, playerId]);
}
function removePlayer(playerId: string) {
setSelectedPlayerIds(selectedPlayerIds.filter((id) => id !== playerId));
if (captainId === playerId) setCaptainId(null);
}
function swapPlayers(playerId: string, direction: "left" | "right") {
const currentIndex = selectedPlayerIds.indexOf(playerId);
if (currentIndex === -1) return;
const player = players.find((p) => p.id === playerId);
if (!player) return;
// پیدا کردن بازیکنان هم‌پست
const samePositionIds = selectedPlayerIds.filter((id) => {
const p = players.find((pl) => pl.id === id);
return p?.position === player.position;
});
const positionIndex = samePositionIds.indexOf(playerId);
const targetIndex = direction === "left" ? positionIndex - 1 : positionIndex + 1;
if (targetIndex < 0 || targetIndex >= samePositionIds.length) return;
const targetPlayerId = samePositionIds[targetIndex];
const targetGlobalIndex = selectedPlayerIds.indexOf(targetPlayerId);
// جابجایی
const newIds = [...selectedPlayerIds];
newIds[currentIndex] = targetPlayerId;
newIds[targetGlobalIndex] = playerId;
setSelectedPlayerIds(newIds);
}
function handleDragStart(event: DragStartEvent) {
const id = event.active.id as string;
setActiveId(id);
}
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
setActiveId(null);
if (!over) return;
const activePlayerId = active.id as string;
const overTarget = over.id as string;
if (activePlayerId === overTarget) return;
const activePlayer = players.find((p) => p.id === activePlayerId);
if (!activePlayer) return;
const activeInLineup = selectedPlayerIds.includes(activePlayerId);
// اگر روی "add-zone" رها شد
if (overTarget.startsWith("add-zone-")) {
const position = overTarget.replace("add-zone-", "");
if (activePlayer.position === position && !activeInLineup) {
addPlayer(activePlayerId, position);
}
return;
}
// اگر روی بازیکن دیگری رها شد
const overPlayer = players.find((p) => p.id === overTarget);
if (!overPlayer) return;
const overInLineup = selectedPlayerIds.includes(overTarget);
// جابجایی دو بازیکن در ترکیب
if (activeInLineup && overInLineup && activePlayer.position === overPlayer.position) {
const oldIndex = selectedPlayerIds.indexOf(activePlayerId);
const newIndex = selectedPlayerIds.indexOf(overTarget);
setSelectedPlayerIds(arrayMove(selectedPlayerIds, oldIndex, newIndex));
}
// جایگزینی: بازیکن از لیست روی بازیکن در ترکیب
else if (!activeInLineup && overInLineup && activePlayer.position === overPlayer.position) {
const newIds = selectedPlayerIds.map(id => id === overTarget ? activePlayerId : id);
setSelectedPlayerIds(newIds);
if (captainId === overTarget) setCaptainId(null);
}
}
async function handleSave() {
if (selectedPlayerIds.length !== 11) {
setMsg({ text: "باید دقیقاً 11 بازیکن انتخاب کنید", type: "error" });
return;
}
if (selectedGk.length !== 1 || selectedDef.length !== fmt.def ||
selectedMid.length !== fmt.mid || selectedFwd.length !== fmt.fwd) {
setMsg({ text: `ترکیب باید ${formation} باشد`, type: "error" });
return;
}
setLoading(true);
const res = await fetch(`/api/countries/${country.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
defaultFormation: formation,
defaultLineupPlayerIds: selectedPlayerIds,
defaultCaptainId: captainId,
}),
});
if (res.ok) {
setMsg({ text: "ترکیب پیش‌فرض ذخیره شد", type: "success" });
router.refresh();
} else {
const data = await res.json();
setMsg({ text: data.error || "خطا در ذخیره", type: "error" });
}
setLoading(false);
}
const activePlayer = activeId ? players.find((p) => p.id === activeId) : null;
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="grid grid-cols-[300px_1fr_320px] gap-6">
{/* ستون چپ: انتخاب فرمیشن */}
<div className="bg-white rounded-2xl shadow p-6 h-fit sticky top-6">
<h2 className="text-lg font-bold mb-4">انتخاب فرمیشن</h2>
<div className="flex flex-col gap-2">
{Object.entries(FORMATIONS).map(([key, val]) => (
<button key={key} onClick={() => handleFormationChange(key)}
className={`py-3 rounded-xl text-sm font-bold border-2 transition ${
formation === key
? "bg-green-700 text-white border-green-700"
: "bg-white border-gray-200 hover:border-green-400"
}`}>
{key} ({val.label})
</button>
))}
</div>
<div className="mt-6 p-4 bg-gray-50 rounded-xl">
<div className="text-sm text-gray-600 mb-2">انتخاب شده:</div>
<div className="text-lg font-bold text-green-700">{selectedPlayerIds.length} / 11</div>
<div className="text-xs text-gray-500 mt-2">
GK: {selectedGk.length}/1 · DEF: {selectedDef.length}/{fmt.def} ·
MID: {selectedMid.length}/{fmt.mid} · FWD: {selectedFwd.length}/{fmt.fwd}
</div>
</div>
{msg && (
<div className={`mt-4 px-3 py-2 rounded-lg text-sm ${
msg.type === "error" ? "bg-red-50 text-red-600" : "bg-green-50 text-green-700"
}`}>
{msg.text}
</div>
)}
<button onClick={handleSave} disabled={loading || selectedPlayerIds.length !== 11}
className="w-full mt-4 bg-green-700 text-white py-3 rounded-xl font-bold hover:bg-green-800 transition disabled:opacity-50">
{loading ? "در حال ذخیره..." : "ذخیره ترکیب پیش‌فرض"}
</button>
</div>
{/* ستون وسط: زمین */}
<div className="bg-gradient-to-b from-green-700 to-green-900 rounded-2xl shadow p-6 relative overflow-hidden min-h-[700px]">
<div className="absolute inset-0 opacity-10 flex items-center justify-center">
<svg width="100%" height="100%" className="absolute inset-0">
{/* خط وسط افقی */}
<line x1="0" y1="50%" x2="100%" y2="50%" stroke="white" strokeWidth="2" vectorEffect="non-scaling-stroke" />
{/* دایره وسط */}
<circle cx="50%" cy="50%" r="70" stroke="white" strokeWidth="2" fill="none" vectorEffect="non-scaling-stroke" />
</svg>
</div>
<div className="relative z-10 flex flex-col gap-4 h-full justify-around py-4">
<PositionRow
title="FWD"
players={fwdPlayers}
selectedIds={selectedFwd}
maxCount={fmt.fwd}
captainId={captainId}
activePlayerId={activeId}
onRemove={removePlayer}
onSetCaptain={setCaptainId}
onSwap={swapPlayers}
/>
<PositionRow
title="MID"
players={midPlayers}
selectedIds={selectedMid}
maxCount={fmt.mid}
captainId={captainId}
activePlayerId={activeId}
onRemove={removePlayer}
onSetCaptain={setCaptainId}
onSwap={swapPlayers}
/>
<PositionRow
title="DEF"
players={defPlayers}
selectedIds={selectedDef}
maxCount={fmt.def}
captainId={captainId}
activePlayerId={activeId}
onRemove={removePlayer}
onSetCaptain={setCaptainId}
onSwap={swapPlayers}
/>
<PositionRow
title="GK"
players={gkPlayers}
selectedIds={selectedGk}
maxCount={1}
captainId={captainId}
activePlayerId={activeId}
onRemove={removePlayer}
onSetCaptain={setCaptainId}
onSwap={swapPlayers}
/>
</div>
</div>
{/* ستون راست: لیست بازیکنان */}
<div className="bg-white rounded-2xl shadow p-6 max-h-[800px] overflow-y-auto h-fit sticky top-6">
<h2 className="text-lg font-bold mb-4">بازیکنان موجود</h2>
{["GK", "DEF", "MID", "FWD"].map((pos) => {
const posList = players.filter((p) => p.position === pos);
const available = posList.filter((p) => !selectedPlayerIds.includes(p.id));
return (
<div key={pos} className="mb-6">
<h3 className="text-sm font-bold text-gray-700 mb-3">{POSITION_LABELS[pos]}</h3>
<div className="flex gap-2 overflow-x-auto pb-2">
{available.map((p) => (
<PlayerCard
key={p.id}
player={p}
onAdd={() => addPlayer(p.id, pos)}
isDragging={activeId === p.id}
/>
))}
{available.length === 0 && (
<div className="text-xs text-gray-400 py-4">همه بازیکنان انتخاب شدهاند</div>
)}
</div>
</div>
);
})}
</div>
</div>
<DragOverlay>
{activePlayer ? (
<div className="bg-white rounded-xl p-2 shadow-2xl opacity-90">
<PlayerCardContent player={activePlayer} />
</div>
) : null}
</DragOverlay>
{/* دیالوگ حذف بازیکنان اضافی */}
{showRemoveDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={handleCancelFormationChange}>
<div className="bg-white rounded-2xl p-6 max-w-md w-full mx-4 shadow-2xl" onClick={(e) => e.stopPropagation()}>
<h3 className="text-xl font-bold mb-4 text-gray-800">
تغییر فرمیشن به {pendingFormation}
</h3>
<p className="text-sm text-gray-600 mb-4">
برای تغییر به فرمیشن جدید، باید {playersToRemove.count} بازیکن از پست {POSITION_LABELS[playersToRemove.position]} حذف کنید:
</p>
<div className="space-y-2 mb-6 max-h-64 overflow-y-auto">
{playersToRemove.players.map((p) => {
const isSelected = selectedPlayerIds.includes(p.id);
if (!isSelected) return null;
return (
<div key={p.id} className="flex items-center gap-3 p-3 bg-gray-50 rounded-xl hover:bg-gray-100 transition">
<div className="relative w-12 h-12 rounded-lg overflow-hidden bg-gray-200 flex-shrink-0">
{p.image ? (
<Image
src={`/uploads/players/${p.image}`}
alt={p.name}
fill
className="object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400">
👤
</div>
)}
</div>
<div className="flex-1">
<div className="font-bold text-sm text-gray-800">{p.name}</div>
<div className="text-xs text-gray-500">{POSITION_LABELS[p.position]}</div>
</div>
<button
onClick={() => handleRemovePlayer(p.id)}
className="bg-red-500 text-white px-3 py-1.5 rounded-lg text-xs font-bold hover:bg-red-600 transition"
>
حذف
</button>
</div>
);
})}
</div>
<div className="flex gap-3">
<button
onClick={handleCancelFormationChange}
className="flex-1 bg-gray-200 text-gray-700 py-3 rounded-xl font-bold hover:bg-gray-300 transition"
>
انصراف
</button>
</div>
</div>
</div>
)}
</DndContext>
);
}
// کامپوننت کارت بازیکن در لیست
function PlayerCard({ player, onAdd, isDragging }: {
player: Player;
onAdd: () => void;
isDragging: boolean;
}) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: player.id,
});
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
} : undefined;
return (
<div
ref={setNodeRef}
style={{ width: "80px", ...style }}
{...listeners}
{...attributes}
className={`flex-shrink-0 bg-gray-50 rounded-xl p-2 cursor-move hover:bg-gray-100 transition border-2 ${
isDragging ? "border-green-500 opacity-50" : "border-transparent"
}`}
>
<PlayerCardContent player={player} />
<button
onClick={onAdd}
onPointerDown={(e) => e.stopPropagation()}
className="w-full mt-2 bg-green-600 text-white text-xs py-1 rounded-lg hover:bg-green-700 transition"
>
+ افزودن
</button>
</div>
);
}
// محتوای کارت بازیکن
function PlayerCardContent({ player }: { player: Player }) {
const positionColor = POSITION_COLORS[player.position] || "bg-gray-100 text-gray-800";
return (
<>
<div className="relative w-16 h-16 rounded-lg overflow-hidden bg-gray-200 mb-1 mx-auto">
{player.image ? (
<Image
src={`/uploads/players/${player.image}`}
alt={player.name}
fill
className="object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400 text-2xl">
👤
</div>
)}
</div>
<div className="text-[10px] font-bold text-gray-800 text-center leading-tight">
{player.name.split(" ").slice(-1)[0]}
</div>
<div className={`text-[8px] text-center font-bold mt-1 rounded px-1 py-0.5 ${positionColor}`}>
{player.position}
</div>
</>
);
}
// ردیف بازیکنان در زمین
function PositionRow({ title, players, selectedIds, maxCount, captainId, activePlayerId, onRemove, onSetCaptain, onSwap }: {
title: string;
players: Player[];
selectedIds: string[];
maxCount: number;
captainId: string | null;
activePlayerId: string | null;
onRemove: (id: string) => void;
onSetCaptain: (id: string | null) => void;
onSwap: (id: string, direction: "left" | "right") => void;
}) {
// ترتیب بازیکنان رو حفظ می‌کنیم
const selected = selectedIds
.map(id => players.find(p => p.id === id))
.filter((p): p is Player => p !== undefined && p.position === title);
const activePlayer = activePlayerId ? players.find((p) => p.id === activePlayerId) : null;
const canAcceptDrop = activePlayer?.position === title;
const positionColor = POSITION_COLORS[title] || "bg-gray-100 text-gray-800";
return (
<div className="text-center">
<div className="text-white text-xs font-bold mb-2 opacity-70">{title} ({selected.length}/{maxCount})</div>
<div className="flex justify-center gap-3">
{selected.map((p, index) => (
<FieldPlayerCard
key={p.id}
player={p}
title={title}
positionColor={positionColor}
captainId={captainId}
activePlayerId={activePlayerId}
canAcceptDrop={canAcceptDrop}
onRemove={onRemove}
onSetCaptain={onSetCaptain}
onSwap={onSwap}
canMoveLeft={index > 0}
canMoveRight={index < selected.length - 1}
/>
))}
{/* جاهای خالی */}
{Array.from({ length: maxCount - selected.length }).map((_, i) => (
<EmptySlot
key={`empty-${title}-${i}`}
id={`add-zone-${title}`}
canAcceptDrop={canAcceptDrop}
activePlayerId={activePlayerId}
/>
))}
</div>
</div>
);
}
// کامپوننت بازیکن در زمین
function FieldPlayerCard({ player, title, positionColor, captainId, activePlayerId, canAcceptDrop, onRemove, onSetCaptain, onSwap, canMoveLeft, canMoveRight }: {
player: Player;
title: string;
positionColor: string;
captainId: string | null;
activePlayerId: string | null;
canAcceptDrop: boolean;
onRemove: (id: string) => void;
onSetCaptain: (id: string | null) => void;
onSwap: (id: string, direction: "left" | "right") => void;
canMoveLeft: boolean;
canMoveRight: boolean;
}) {
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
id: player.id,
});
const { setNodeRef: setDropRef, isOver } = useDroppable({
id: player.id,
});
const style = transform ? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
zIndex: 50,
} : undefined;
return (
<div
ref={setDropRef}
className={`relative group ${canAcceptDrop && isOver ? "ring-2 ring-yellow-300 rounded-xl" : ""}`}
>
<div
ref={setNodeRef}
style={style}
{...listeners}
{...attributes}
className={`bg-white/95 rounded-xl p-2 cursor-move hover:bg-white transition shadow-lg ${
isDragging ? "opacity-50" : ""
}`}
>
{/* دکمه‌های چپ و راست */}
<div className="absolute -top-2 left-0 right-0 flex justify-between px-1 opacity-0 group-hover:opacity-100 transition z-10">
{canMoveLeft && (
<button
onClick={(e) => {
e.stopPropagation();
onSwap(player.id, "left");
}}
onPointerDown={(e) => e.stopPropagation()}
className="bg-blue-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs font-bold shadow hover:bg-blue-600"
>
</button>
)}
<div className="flex-1"></div>
{canMoveRight && (
<button
onClick={(e) => {
e.stopPropagation();
onSwap(player.id, "right");
}}
onPointerDown={(e) => e.stopPropagation()}
className="bg-blue-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs font-bold shadow hover:bg-blue-600"
>
</button>
)}
</div>
<div className="relative w-12 h-12 rounded-lg overflow-hidden bg-gray-200 mb-1">
{player.image ? (
<Image
src={`/uploads/players/${player.image}`}
alt={player.name}
fill
className="object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-gray-400 text-xs">
👤
</div>
)}
</div>
<div className="text-[10px] font-bold text-gray-800 text-center leading-tight">
{player.name.split(" ").slice(-1)[0]}
</div>
<div className={`text-[8px] text-center font-bold mt-1 rounded px-1 py-0.5 ${positionColor}`}>
{title}
</div>
{captainId === player.id && (
<div className="absolute -top-1 -right-1 bg-yellow-400 text-yellow-900 rounded-full w-5 h-5 flex items-center justify-center text-xs font-bold shadow">
C
</div>
)}
</div>
<div className="absolute -bottom-8 left-1/2 -translate-x-1/2 opacity-0 group-hover:opacity-100 transition flex gap-1 z-20">
<button
onClick={(e) => {
e.stopPropagation();
onSetCaptain(captainId === player.id ? null : player.id);
}}
className="bg-yellow-400 text-yellow-900 text-[8px] px-2 py-0.5 rounded-full font-bold whitespace-nowrap shadow">
{captainId === player.id ? "❌" : "C"}
</button>
<button
onClick={(e) => {
e.stopPropagation();
onRemove(player.id);
}}
className="bg-red-500 text-white text-[8px] px-2 py-0.5 rounded-full font-bold shadow">
حذف
</button>
</div>
</div>
);
}
// کامپوننت جای خالی
function EmptySlot({ id, canAcceptDrop, activePlayerId }: {
id: string;
canAcceptDrop: boolean;
activePlayerId: string | null;
}) {
const { setNodeRef, isOver } = useDroppable({
id: id,
});
return (
<div
ref={setNodeRef}
className={`border-2 border-dashed rounded-xl w-16 h-20 flex items-center justify-center transition ${
canAcceptDrop && activePlayerId
? isOver
? "border-yellow-300 bg-yellow-400/30 scale-105"
: "border-yellow-300 bg-yellow-400/20 animate-pulse"
: "border-white/30"
}`}
>
<span className="text-xs text-white/30">+</span>
</div>
);
}