795 lines
28 KiB
TypeScript
795 lines
28 KiB
TypeScript
"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>
|
||
);
|
||
}
|