diff --git a/package.json b/package.json index c0fa4d5bd..328d64777 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "@algolia/autocomplete-js": "1.7.1", "@algolia/autocomplete-theme-classic": "1.7.1", "@apollo/client": "^3.6.9", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", "@google-cloud/storage": "^6.9.5", "@headlessui/react": "^1.6.4", "@heroicons/react": "2.0.13", diff --git a/src/components/edit/AreaCRUD.tsx b/src/components/edit/AreaCRUD.tsx index 95f342d64..ff3b38ee8 100644 --- a/src/components/edit/AreaCRUD.tsx +++ b/src/components/edit/AreaCRUD.tsx @@ -5,9 +5,24 @@ import { DeleteAreaTrigger, AddAreaTrigger, AddAreaTriggerButtonMd, AddAreaTrigg import { AreaEntityIcon } from '../EntityIcons' import NetworkSquareIcon from '../../assets/icons/network-square-icon.svg' import React, { useState } from 'react' -import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' import useUpdateAreasCmd from '../../js/hooks/useUpdateAreasCmd' import { useSession } from 'next-auth/react' +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + DragOverlay +} from '@dnd-kit/core' +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + rectSortingStrategy +} from '@dnd-kit/sortable' +import { SortableItem } from './SortableItem' export type AreaCRUDProps = Pick & { childAreas: AreaType[] @@ -23,38 +38,42 @@ export type AreaCRUDProps = Pick & { export const AreaCRUD = ({ uuid: parentUuid, areaName: parentName, childAreas, editMode, onChange }: AreaCRUDProps): JSX.Element => { const session = useSession() const areaCount = childAreas.length + // Prepare {uuid: } mapping to avoid passing entire areas around. + const areaStore = new Map(childAreas.map(a => [a.uuid, a])) - const [childAreasState, setChildAreasState] = useState(childAreas) + const [areasSortedState, setAreasSortedState] = useState(Array.from(areaStore.keys())) + const [draggedArea, setDraggedArea] = useState(null) const { updateAreasSortingOrderCmd } = useUpdateAreasCmd({ areaId: parentUuid, accessToken: session?.data?.accessToken as string ?? '' }) - function reorder (list: T[], startIndex: number, endIndex: number): T[] { - const result = Array.from(list) - const [removed] = result.splice(startIndex, 1) - result.splice(endIndex, 0, removed) - return result - } + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates + }) + ) - function onDragEnd (result): void { - /* if (!result.destination) { - return - } */ + function handleDragEnd (event): void { + const { active, over } = event + setDraggedArea(null) - if (result.destination.index === result.source.index) { - return + if (active.id !== over.id) { + const oldIndex = areasSortedState.indexOf(active.id) + const newIndex = areasSortedState.indexOf(over.id) + const reorderedChildAreas = arrayMove(areasSortedState, oldIndex, newIndex) + void updateAreasSortingOrderCmd(reorderedChildAreas.map((uuid, idx) => ({ areaId: uuid, leftRightIndex: idx }))) + setAreasSortedState(reorderedChildAreas) } + } - const reorderedChildAreas = reorder( - childAreasState, - result.source.index, - result.destination.index - ) - - void updateAreasSortingOrderCmd(reorderedChildAreas.map((area, idx) => ({ areaId: area.uuid, leftRightIndex: idx }))) - setChildAreasState(reorderedChildAreas) + function handleDragStart (event): void { + const { active } = event + if (active.id != null) { + setDraggedArea(active.id) + } } return ( @@ -71,7 +90,7 @@ export const AreaCRUD = ({ uuid: parentUuid, areaName: parentName, childAreas, e {areaCount > 0 && `Total: ${areaCount}`} -
+
{areaCount === 0 && (
@@ -79,54 +98,55 @@ export const AreaCRUD = ({ uuid: parentUuid, areaName: parentName, childAreas, e {editMode && }
)} - {childAreasState.length > 0 && ( - 0 && ( + - - {(provided, snapshot) => ( -
- {childAreasState.map((i, idx) => ( - + + {areasSortedState.map((uuid, idx) => ( + + - {(provided, snapshot) => ( -
- -
- )} -
- ))} -
- )} -
-
+ borderBottom={[Math.ceil(areaCount / 2) - 1, areaCount - 1].includes(idx)} + parentUuid={parentUuid} + {/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ + ...areaStore.get(uuid)!} + editMode={editMode} + onChange={onChange} + /> + + ))} + + + + {draggedArea != null + ? ( +
+ +
+ ) + : null} +
+ )} - {areaCount > 0 && editMode && (
@@ -156,9 +176,9 @@ export const AreaItem = ({ index, borderBottom, areaName, uuid, parentUuid, onCh const { totalClimbs, metadata: { leaf, isBoulder } } = props const isLeaf = leaf || isBoulder return ( -
+
- {index + 1} + {index != null && index + 1}
diff --git a/src/components/edit/SortableItem.tsx b/src/components/edit/SortableItem.tsx new file mode 100644 index 000000000..3e58493a5 --- /dev/null +++ b/src/components/edit/SortableItem.tsx @@ -0,0 +1,30 @@ +import { CSS } from '@dnd-kit/utilities' +import { useSortable } from '@dnd-kit/sortable' + +interface SortableItemProps { + id: string + children: JSX.Element +} + +export const SortableItem = (props: SortableItemProps): JSX.Element => { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging + } = useSortable({ id: props.id }) + + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.3 : 1 + } + + return ( +
+ {props.children} +
+ ) +} diff --git a/src/styles/defaults.css b/src/styles/defaults.css index f1f3622c9..a6d817b54 100644 --- a/src/styles/defaults.css +++ b/src/styles/defaults.css @@ -432,11 +432,11 @@ table td.dtable-my-range { /** 2-column layout for Area and Climb list */ .two-column-table { - @apply mt-16 lg:gap-x-24 lg:columns-2; + @apply lg:gap-x-24 lg:columns-2; } .area-row { - @apply py-4 flex flex-row flex-nowrap gap-4 items-start border-t break-inside-avoid-column break-inside-avoid; + @apply py-4 flex flex-row flex-nowrap gap-4 px-4 items-start border-t break-inside-avoid-column break-inside-avoid; } .area-entity-box {