Skip to content

Commit

Permalink
Use dnd-kit
Browse files Browse the repository at this point in the history
  • Loading branch information
zichongkao committed May 18, 2023
1 parent b35c3ab commit 9831fcc
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 72 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
160 changes: 90 additions & 70 deletions src/components/edit/AreaCRUD.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<AreaType, 'uuid' | 'areaName'> & {
childAreas: AreaType[]
Expand All @@ -23,38 +38,42 @@ export type AreaCRUDProps = Pick<AreaType, 'uuid' | 'areaName'> & {
export const AreaCRUD = ({ uuid: parentUuid, areaName: parentName, childAreas, editMode, onChange }: AreaCRUDProps): JSX.Element => {
const session = useSession()
const areaCount = childAreas.length
// Prepare {uuid: <AreaType>} mapping to avoid passing entire areas around.
const areaStore = new Map(childAreas.map(a => [a.uuid, a]))

const [childAreasState, setChildAreasState] = useState<AreaType[]>(childAreas)
const [areasSortedState, setAreasSortedState] = useState<string[]>(Array.from(areaStore.keys()))
const [draggedArea, setDraggedArea] = useState<string | null>(null)

const { updateAreasSortingOrderCmd } = useUpdateAreasCmd({
areaId: parentUuid,
accessToken: session?.data?.accessToken as string ?? ''
})

function reorder<T> (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 (
Expand All @@ -71,62 +90,63 @@ export const AreaCRUD = ({ uuid: parentUuid, areaName: parentName, childAreas, e
<span className='text-base-300 text-sm'>{areaCount > 0 && `Total: ${areaCount}`}</span>
</div>

<hr className='mt-4 mb-8 border-1 border-base-content' />
<hr className='mt-4 border-1 border-base-content' />

{areaCount === 0 && (
<div>
<div className='mb-8 italic text-base-300'>This area doesn't have any child areas.</div>
{editMode && <AddAreaTrigger parentName={parentName} parentUuid={parentUuid} onSuccess={onChange} />}
</div>)}

{childAreasState.length > 0 && (
<DragDropContext
onDragEnd={onDragEnd}
{areaCount > 0 && (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
>
<Droppable droppableId='cragTable'>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={`two-column-table ${editMode ? '' : 'xl:grid-cols-2 lg:grid-cols-2 md:grid-cols-2'
} fr-2`}
>
{childAreasState.map((i, idx) => (
<Draggable
isDragDisabled={!editMode}
draggableId={i.uuid}
<div
className={`two-column-table ${editMode ? '' : 'xl:grid-cols-2 lg:grid-cols-2 md:grid-cols-2'
} fr-2`}
>
<SortableContext
items={areasSortedState}
strategy={rectSortingStrategy}
>
{areasSortedState.map((uuid, idx) => (
<SortableItem id={uuid} key={uuid}>
<AreaItem
index={idx}
key={i.uuid}
>
{(provided, snapshot) => (
<div
className={
`${snapshot.isDragging ? 'bg-purple-100' : 'bg-white'}`
}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<AreaItem
{...provided.draggableProps}
key={i.uuid}
index={idx}
borderBottom={idx === Math.ceil(areaCount / 2) - 1}
parentUuid={parentUuid}
{...i}
editMode={editMode}
onChange={onChange}
/>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
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}
/>
</SortableItem>
))}
</SortableContext>
</div>
<DragOverlay>
{draggedArea != null
? (
<div className='bg-purple-100'>
<AreaItem
index={areasSortedState.indexOf(draggedArea)}
borderBottom
parentUuid={parentUuid}
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
...areaStore.get(draggedArea)!}
editMode={editMode}
onChange={onChange}
/>
</div>
)
: null}
</DragOverlay>
</DndContext>
)}

{areaCount > 0 && editMode && (
<div className='mt-8 md:text-right'>
<AddAreaTrigger parentName={parentName} parentUuid={parentUuid} onSuccess={onChange}>
Expand Down Expand Up @@ -156,9 +176,9 @@ export const AreaItem = ({ index, borderBottom, areaName, uuid, parentUuid, onCh
const { totalClimbs, metadata: { leaf, isBoulder } } = props
const isLeaf = leaf || isBoulder
return (
<div className={clx('area-row')}>
<div className={clx('area-row', borderBottom ? 'border-b' : '')}>
<a href={`/crag/${uuid}`} className='area-entity-box'>
{index + 1}
{index != null && index + 1}
</a>
<a href={`/crag/${uuid}`} className='flex flex-col items-start items-stretch grow gap-y-1'>
<div className='font-semibold uppercase thick-link'>
Expand Down
30 changes: 30 additions & 0 deletions src/components/edit/SortableItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{props.children}
</div>
)
}
4 changes: 2 additions & 2 deletions src/styles/defaults.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 9831fcc

Please sign in to comment.