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
+2
View File
@@ -14,6 +14,8 @@
},
"dependencies": {
"@auth/mongodb-adapter": "^3.5.2",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@headlessui-float/react": "^0.15.0",
"@headlessui/react": "^2.1.8",
"@headlessui/tailwindcss": "^0.2.1",
@@ -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>
+36 -10
View File
@@ -7,11 +7,6 @@
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63"
integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==
"@ag-media/react-pdf-table@^2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@ag-media/react-pdf-table/-/react-pdf-table-2.0.3.tgz#113554b583b46e41a098cf64fecb5decd59ba004"
integrity sha512-IscjfAOKwsyQok9YmzvuToe6GojN7J8hF0kb8C+K8qZX1DvhheGO+hRSAPxbv2nKMbSpvk7CIhSqJEkw++XVWg==
"@alloc/quick-lru@^5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
@@ -1127,6 +1122,37 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@dnd-kit/accessibility@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz#3b4202bd6bb370a0730f6734867785919beac6af"
integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==
dependencies:
tslib "^2.0.0"
"@dnd-kit/core@^6.3.1":
version "6.3.1"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-6.3.1.tgz#4c36406a62c7baac499726f899935f93f0e6d003"
integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==
dependencies:
"@dnd-kit/accessibility" "^3.1.1"
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/sortable@^10.0.0":
version "10.0.0"
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-10.0.0.tgz#1f9382b90d835cd5c65d92824fa9dafb78c4c3e8"
integrity sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==
dependencies:
"@dnd-kit/utilities" "^3.2.2"
tslib "^2.0.0"
"@dnd-kit/utilities@^3.2.2":
version "3.2.2"
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz#5a32b6af356dc5f74d61b37d6f7129a4040ced7b"
integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==
dependencies:
tslib "^2.0.0"
"@emnapi/runtime@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.2.0.tgz#71d018546c3a91f3b51106530edbc056b9f2f2e3"
@@ -6704,16 +6730,16 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@^2.0.0, tslib@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
tslib@^2.4.0, tslib@^2.6.2:
version "2.7.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
tslib@^2.8.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"