drag and drop order of players

This commit is contained in:
2025-12-12 11:25:03 +01:00
parent 56ef46d8f1
commit 3f58f7fdbb
4 changed files with 177 additions and 44 deletions
@@ -0,0 +1,55 @@
import { CSSProperties, type ReactNode } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
function SortablePlayerRow({
field,
index,
renderField,
}: {
field: any;
index: number;
renderField: (field: any, index: number) => ReactNode;
}) {
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: field.id });
const style: CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
marginBottom: "8px",
background: "#fff",
border: "1px solid #ccc",
padding: "8px",
opacity: isDragging ? 0.8 : 1,
};
return (
<div ref={setNodeRef} style={style} {...attributes}>
<div className="flex items-center gap-2">
<button
type="button"
ref={setActivatorNodeRef}
{...listeners}
className="cursor-grab select-none rounded border border-gray-300 bg-gray-50 px-2 py-1 text-sm text-gray-700 active:cursor-grabbing"
title="Drag to reorder"
aria-label="Drag to reorder"
>
</button>
<div className="flex-1">{renderField(field, index)}</div>
</div>
</div>
);
}
export default SortablePlayerRow;
@@ -2,6 +2,17 @@
import { useEffect, useState } from "react";
import {
DndContext,
PointerSensor,
closestCenter,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { useAtom } from "jotai";
import {
Controller,
@@ -15,6 +26,8 @@ import Select from "react-select";
import { clubPlayersAtom } from "@/atoms";
import { fetchClubPlayers } from "@/server/fetchClubPlayers";
import SortablePlayerRow from "./SortablePlayerRow";
interface IProps {
name: string;
label: string;
@@ -32,11 +45,17 @@ const TeamSelection = ({ name, label, clubOptions, className }: IProps) => {
const { control } = useFormContext();
const [teamsPlayers, setTeamsPlayers] = useAtom(clubPlayersAtom);
const { fields, append, remove } = useFieldArray({
const { fields, append, remove, move } = useFieldArray({
control,
name: `${name}_players`, // e.g., "team1_players"
});
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 6 },
}),
);
const selectedDbId = useWatch({
control,
name: name,
@@ -118,42 +137,73 @@ const TeamSelection = ({ name, label, clubOptions, className }: IProps) => {
)}
/>
<div className="space-y-3">
{fields.map((field, index) => (
<div key={field.id} className="flex items-center gap-2">
<div className="flex-1">
<Controller
name={`${name}_players.${index}`} // Saves as { playerId: "nrFFE_123" }
control={control}
render={({ field: selectField }) => (
<Select
{...selectField}
instanceId={`${name}-player-${index}`}
options={playerOptions}
isSearchable
placeholder="Search for a player..."
onChange={(option) => selectField.onChange(option?.value)}
value={playerOptions.find(
(op) => op.value === selectField.value,
)}
classNames={{
control: () => "border-gray-300 rounded-md shadow-sm",
}}
/>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={({ active, over }) => {
if (!over) return;
if (active.id === over.id) return;
const oldIndex = fields.findIndex((f) => f.id === active.id);
const newIndex = fields.findIndex((f) => f.id === over.id);
if (oldIndex === -1 || newIndex === -1) return;
move(oldIndex, newIndex);
}}
>
<SortableContext
items={fields.map((f) => f.id)}
strategy={verticalListSortingStrategy}
>
<div className="space-y-3">
{fields.map((field, index) => (
<SortablePlayerRow
key={field.id}
field={field}
index={index}
renderField={(_field, i) => (
<div className="flex items-center gap-2">
<div className="flex-1">
<Controller
name={`${name}_players.${i}`}
control={control}
render={({ field: selectField }) => (
<Select
{...selectField}
instanceId={`${name}-player-${i}`}
options={playerOptions}
isSearchable
placeholder="Search for a player..."
onChange={(option) =>
selectField.onChange(option?.value)
}
value={playerOptions.find(
(op) => op.value === selectField.value,
)}
classNames={{
control: () =>
"border-gray-300 rounded-md shadow-sm",
}}
/>
)}
/>
</div>
<button
type="button"
onClick={() => remove(i)}
className="p-2 text-red-500 hover:text-red-700"
title="Remove player"
>
<FaMinus />
</button>
</div>
)}
/>
</div>
<button
type="button"
onClick={() => remove(index)}
className="p-2 text-red-500 hover:text-red-700"
title="Remove player"
>
<FaMinus />
</button>
))}
</div>
))}
</div>
</SortableContext>
</DndContext>
{isLoading ? (
<p className="text-sm text-gray-500">Loading players...</p>