mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 12:36:57 +00:00
drag and drop order of players
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user