Skip to content

Commit 5227c19

Browse files
committed
Refactor: Migrate to single drag-and-drop library (@dnd-kit)
1 parent afe95bd commit 5227c19

6 files changed

Lines changed: 323 additions & 324 deletions

File tree

package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
"@dnd-kit/core": "^6.3.1",
4242
"@dnd-kit/sortable": "^8.0.0",
4343
"@dnd-kit/utilities": "^3.2.2",
44-
"@hello-pangea/dnd": "^18.0.1",
4544
"@hookform/resolvers": "^3.10.0",
4645
"@monaco-editor/react": "^4.7.0",
4746
"@testing-library/dom": "^10.4.1",
@@ -73,8 +72,6 @@
7372
"react": "^18.3.1",
7473
"react-big-calendar": "1.19.4",
7574
"react-countdown": "^2.3.6",
76-
"react-dnd": "^16.0.1",
77-
"react-dnd-html5-backend": "^16.0.1",
7875
"react-dom": "^18.3.1",
7976
"react-hook-form": "^7.60.0",
8077
"react-hot-toast": "^2.6.0",

src/components/drag-drop/DragDropContainer.tsx

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
'use client';
22

3-
import React from 'react';
4-
import { DndProvider } from 'react-dnd';
5-
import { HTML5Backend } from 'react-dnd-html5-backend';
3+
import React, { useState } from 'react';
4+
import {
5+
DndContext,
6+
closestCenter,
7+
KeyboardSensor,
8+
PointerSensor,
9+
useSensor,
10+
useSensors,
11+
DragEndEvent,
12+
DragStartEvent,
13+
DragOverEvent,
14+
DragOverlay,
15+
} from '@dnd-kit/core';
16+
import { sortableKeyboardCoordinates } from '@dnd-kit/sortable';
617
import { useDragDrop } from '../../hooks/useDragDrop';
718
import { DragDropItem, DragDropZone } from '../../utils/dragDropUtils';
8-
import { DragPreview } from './DragPreview';
919
import { DropZones } from './DropZones';
20+
import { DragPreview } from './DragPreview';
1021

1122
interface DragDropContainerProps {
1223
title?: string;
@@ -44,8 +55,113 @@ export const DragDropContainer = ({
4455
onAutoSave,
4556
});
4657

58+
const [activeItem, setActiveItem] = useState<DragDropItem | null>(null);
59+
60+
const sensors = useSensors(
61+
useSensor(PointerSensor, {
62+
activationConstraint: {
63+
distance: 5,
64+
},
65+
}),
66+
useSensor(KeyboardSensor, {
67+
coordinateGetter: sortableKeyboardCoordinates,
68+
})
69+
);
70+
71+
const handleDragStart = (event: DragStartEvent) => {
72+
const { active } = event;
73+
if (active.data.current?.type === 'COURSE_CONTENT_ITEM') {
74+
setActiveItem(active.data.current.item as DragDropItem);
75+
}
76+
};
77+
78+
const handleDragOver = (event: DragOverEvent) => {
79+
const { active, over } = event;
80+
if (!over) return;
81+
82+
const activeData = active.data.current;
83+
const overData = over.data.current;
84+
85+
if (!activeData || !overData) return;
86+
87+
const activeItem = activeData.item as DragDropItem;
88+
const activeZoneId = activeData.zoneId as string;
89+
90+
const overType = overData.type;
91+
92+
if (overType === 'COURSE_CONTENT_ITEM') {
93+
const overItem = overData.item as DragDropItem;
94+
const overZoneId = overData.zoneId as string;
95+
96+
if (activeZoneId !== overZoneId) {
97+
// Move item to new zone (temporary while dragging, or handled at dragEnd depending on preference)
98+
// For simpler implementation without intermediate state updates, we can just let handleDragEnd deal with it.
99+
// But for smooth dragging between lists, we might need to handle moving items between zones here.
100+
// In useDragDrop, state updates are batched, so calling moveToZone is fine.
101+
102+
// Find index of over item
103+
const overZoneItems = state[overZoneId] || [];
104+
const overIndex = overZoneItems.findIndex((i) => i.id === overItem.id);
105+
106+
moveToZone(activeItem.id, activeZoneId, overZoneId, overIndex);
107+
108+
// Mutate active.data.current so it points to the new zoneId for subsequent events
109+
active.data.current = {
110+
...active.data.current,
111+
zoneId: overZoneId,
112+
};
113+
}
114+
} else if (overType === 'ZONE') {
115+
const overZone = overData.zone as DragDropZone;
116+
117+
if (activeZoneId !== overZone.id) {
118+
const overZoneItems = state[overZone.id] || [];
119+
moveToZone(activeItem.id, activeZoneId, overZone.id, overZoneItems.length);
120+
121+
active.data.current = {
122+
...active.data.current,
123+
zoneId: overZone.id,
124+
};
125+
}
126+
}
127+
};
128+
129+
const handleDragEnd = (event: DragEndEvent) => {
130+
setActiveItem(null);
131+
const { active, over } = event;
132+
if (!over) return;
133+
134+
const activeData = active.data.current;
135+
const overData = over.data.current;
136+
137+
if (!activeData || !overData) return;
138+
139+
const activeZoneId = activeData.zoneId as string;
140+
141+
if (overData.type === 'COURSE_CONTENT_ITEM') {
142+
const overItem = overData.item as DragDropItem;
143+
const overZoneId = overData.zoneId as string;
144+
145+
if (activeZoneId === overZoneId) {
146+
const zoneItems = state[activeZoneId] || [];
147+
const oldIndex = zoneItems.findIndex(i => i.id === active.id);
148+
const newIndex = zoneItems.findIndex(i => i.id === over.id);
149+
150+
if (oldIndex !== newIndex) {
151+
reorderInZone(activeZoneId, oldIndex, newIndex);
152+
}
153+
}
154+
}
155+
};
156+
47157
return (
48-
<DndProvider backend={HTML5Backend}>
158+
<DndContext
159+
sensors={sensors}
160+
collisionDetection={closestCenter}
161+
onDragStart={handleDragStart}
162+
onDragOver={handleDragOver}
163+
onDragEnd={handleDragEnd}
164+
>
49165
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-5 md:p-6">
50166
<div className="mb-5 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
51167
<div>
@@ -91,11 +207,11 @@ export const DragDropContainer = ({
91207
<DropZones
92208
zones={zones}
93209
state={state}
94-
onReorder={reorderInZone}
95-
onMoveToZone={moveToZone}
96210
/>
97211
</div>
98-
<DragPreview />
99-
</DndProvider>
212+
<DragOverlay>
213+
{activeItem ? <DragPreview item={activeItem} /> : null}
214+
</DragOverlay>
215+
</DndContext>
100216
);
101217
};
Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,16 @@
11
'use client';
22

33
import React from 'react';
4-
import { useDragLayer } from 'react-dnd';
4+
import { DragDropItem } from '../../utils/dragDropUtils';
55

66
interface DragPreviewProps {
7-
getItemTitle?: (item: unknown) => string;
7+
item: DragDropItem;
88
}
99

10-
export const DragPreview = ({ getItemTitle }: DragPreviewProps) => {
11-
const { item, isDragging, currentOffset } = useDragLayer((monitor) => ({
12-
item: monitor.getItem(),
13-
isDragging: monitor.isDragging(),
14-
currentOffset: monitor.getSourceClientOffset(),
15-
}));
16-
17-
if (!isDragging || !currentOffset) {
18-
return null;
19-
}
20-
21-
const title = getItemTitle
22-
? getItemTitle(item)
23-
: typeof item === 'object' && item !== null && 'title' in item
24-
? String((item as { title: string }).title)
25-
: 'Moving item';
26-
10+
export const DragPreview = ({ item }: DragPreviewProps) => {
2711
return (
28-
<div className="pointer-events-none fixed inset-0 z-50">
29-
<div
30-
className="rounded-md border border-sky-300 bg-sky-50 px-3 py-2 text-sm font-medium text-sky-900 shadow-lg"
31-
style={{
32-
transform: `translate(${currentOffset.x + 8}px, ${currentOffset.y + 8}px)`,
33-
position: 'absolute',
34-
}}
35-
>
36-
{title}
37-
</div>
12+
<div className="rounded-md border border-sky-300 bg-sky-50 px-3 py-2 text-sm font-medium text-sky-900 shadow-lg">
13+
{item.title || 'Moving item'}
3814
</div>
3915
);
4016
};

src/components/drag-drop/DropZones.tsx

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,37 @@
11
'use client';
22

33
import React from 'react';
4-
import { useDrop } from 'react-dnd';
4+
import { useDroppable } from '@dnd-kit/core';
55
import { DragDropState, DragDropZone } from '../../utils/dragDropUtils';
6-
import { DRAG_ITEM_TYPE, SortableList } from './SortableList';
6+
import { SortableList } from './SortableList';
77

88
interface DropZonesProps {
99
zones: DragDropZone[];
1010
state: DragDropState;
11-
onReorder: (zoneId: string, fromIndex: number, toIndex: number) => void;
12-
onMoveToZone: (itemId: string, fromZoneId: string, toZoneId: string, toIndex?: number) => void;
13-
}
14-
15-
interface DragPayload {
16-
id: string;
17-
fromZoneId: string;
18-
index: number;
1911
}
2012

2113
const ZonePanel = ({
2214
zone,
2315
itemsCount,
2416
children,
25-
onDropToZone,
2617
}: {
2718
zone: DragDropZone;
2819
itemsCount: number;
2920
children: React.ReactNode;
30-
onDropToZone: (itemId: string, fromZoneId: string, toZoneId: string) => void;
3121
}) => {
32-
const [{ isOver, canDrop }, drop] = useDrop(
33-
() => ({
34-
accept: DRAG_ITEM_TYPE,
35-
drop: (dragged: DragPayload, monitor) => {
36-
if (monitor.didDrop()) {
37-
return;
38-
}
39-
if (dragged.fromZoneId !== zone.id) {
40-
onDropToZone(dragged.id, dragged.fromZoneId, zone.id);
41-
dragged.fromZoneId = zone.id;
42-
dragged.index = itemsCount;
43-
}
44-
},
45-
collect: (monitor) => ({
46-
isOver: monitor.isOver({ shallow: true }),
47-
canDrop: monitor.canDrop(),
48-
}),
49-
}),
50-
[itemsCount, onDropToZone, zone.id],
51-
);
22+
const { isOver, setNodeRef } = useDroppable({
23+
id: zone.id,
24+
data: {
25+
type: 'ZONE',
26+
zone,
27+
},
28+
});
5229

5330
return (
5431
<section
55-
ref={(node) => {
56-
drop(node);
57-
}}
32+
ref={setNodeRef}
5833
className={`rounded-xl border p-4 transition ${
59-
isOver && canDrop ? 'border-sky-400 bg-sky-50' : 'border-slate-200 bg-white'
34+
isOver ? 'border-sky-400 bg-sky-50' : 'border-slate-200 bg-white'
6035
}`}
6136
>
6237
<header className="mb-3 flex items-center justify-between">
@@ -71,27 +46,15 @@ const ZonePanel = ({
7146
);
7247
};
7348

74-
export const DropZones = ({ zones, state, onReorder, onMoveToZone }: DropZonesProps) => {
49+
export const DropZones = ({ zones, state }: DropZonesProps) => {
7550
return (
7651
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
7752
{zones.map((zone) => {
7853
const items = state[zone.id] ?? [];
7954

8055
return (
81-
<ZonePanel
82-
key={zone.id}
83-
zone={zone}
84-
itemsCount={items.length}
85-
onDropToZone={(itemId, fromZoneId, toZoneId) =>
86-
onMoveToZone(itemId, fromZoneId, toZoneId, items.length)
87-
}
88-
>
89-
<SortableList
90-
zoneId={zone.id}
91-
items={items}
92-
onReorder={onReorder}
93-
onMoveToZone={onMoveToZone}
94-
/>
56+
<ZonePanel key={zone.id} zone={zone} itemsCount={items.length}>
57+
<SortableList zoneId={zone.id} items={items} />
9558
</ZonePanel>
9659
);
9760
})}

0 commit comments

Comments
 (0)