mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
drag and drop order of players
This commit is contained in:
@@ -14,6 +14,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/mongodb-adapter": "^3.5.2",
|
"@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-float/react": "^0.15.0",
|
||||||
"@headlessui/react": "^2.1.8",
|
"@headlessui/react": "^2.1.8",
|
||||||
"@headlessui/tailwindcss": "^0.2.1",
|
"@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 { 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 { useAtom } from "jotai";
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@@ -15,6 +26,8 @@ import Select from "react-select";
|
|||||||
import { clubPlayersAtom } from "@/atoms";
|
import { clubPlayersAtom } from "@/atoms";
|
||||||
import { fetchClubPlayers } from "@/server/fetchClubPlayers";
|
import { fetchClubPlayers } from "@/server/fetchClubPlayers";
|
||||||
|
|
||||||
|
import SortablePlayerRow from "./SortablePlayerRow";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -32,11 +45,17 @@ const TeamSelection = ({ name, label, clubOptions, className }: IProps) => {
|
|||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const [teamsPlayers, setTeamsPlayers] = useAtom(clubPlayersAtom);
|
const [teamsPlayers, setTeamsPlayers] = useAtom(clubPlayersAtom);
|
||||||
|
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove, move } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: `${name}_players`, // e.g., "team1_players"
|
name: `${name}_players`, // e.g., "team1_players"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: { distance: 6 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const selectedDbId = useWatch({
|
const selectedDbId = useWatch({
|
||||||
control,
|
control,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -118,42 +137,73 @@ const TeamSelection = ({ name, label, clubOptions, className }: IProps) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<DndContext
|
||||||
{fields.map((field, index) => (
|
sensors={sensors}
|
||||||
<div key={field.id} className="flex items-center gap-2">
|
collisionDetection={closestCenter}
|
||||||
<div className="flex-1">
|
onDragEnd={({ active, over }) => {
|
||||||
<Controller
|
if (!over) return;
|
||||||
name={`${name}_players.${index}`} // Saves as { playerId: "nrFFE_123" }
|
if (active.id === over.id) return;
|
||||||
control={control}
|
|
||||||
render={({ field: selectField }) => (
|
const oldIndex = fields.findIndex((f) => f.id === active.id);
|
||||||
<Select
|
const newIndex = fields.findIndex((f) => f.id === over.id);
|
||||||
{...selectField}
|
|
||||||
instanceId={`${name}-player-${index}`}
|
if (oldIndex === -1 || newIndex === -1) return;
|
||||||
options={playerOptions}
|
move(oldIndex, newIndex);
|
||||||
isSearchable
|
}}
|
||||||
placeholder="Search for a player..."
|
>
|
||||||
onChange={(option) => selectField.onChange(option?.value)}
|
<SortableContext
|
||||||
value={playerOptions.find(
|
items={fields.map((f) => f.id)}
|
||||||
(op) => op.value === selectField.value,
|
strategy={verticalListSortingStrategy}
|
||||||
)}
|
>
|
||||||
classNames={{
|
<div className="space-y-3">
|
||||||
control: () => "border-gray-300 rounded-md shadow-sm",
|
{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>
|
||||||
</div>
|
</DndContext>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<p className="text-sm text-gray-500">Loading players...</p>
|
<p className="text-sm text-gray-500">Loading players...</p>
|
||||||
|
|||||||
@@ -7,11 +7,6 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63"
|
resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63"
|
||||||
integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==
|
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":
|
"@alloc/quick-lru@^5.2.0":
|
||||||
version "5.2.0"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
|
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"
|
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
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":
|
"@emnapi/runtime@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.2.0.tgz#71d018546c3a91f3b51106530edbc056b9f2f2e3"
|
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"
|
minimist "^1.2.6"
|
||||||
strip-bom "^3.0.0"
|
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:
|
tslib@^2.4.0, tslib@^2.6.2:
|
||||||
version "2.7.0"
|
version "2.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
||||||
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
|
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:
|
type-check@^0.4.0, type-check@~0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||||
|
|||||||
Reference in New Issue
Block a user