Skip to content

Commit a5b593c

Browse files
Added a drag and drop feature to the tab edit mode - admin-level access.
1 parent 70143f3 commit a5b593c

2 files changed

Lines changed: 103 additions & 9 deletions

File tree

‎apps/codebility/app/home/announcements/AnnouncementModal.tsx‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,40 @@ export const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
321321
}
322322
};
323323

324+
const [draggingId, setDraggingId] = useState<string | null>(null);
325+
const [dragOverId, setDragOverId] = useState<string | null>(null);
326+
327+
const handleDragStart = (e: React.DragEvent, id: AnnouncementCategory) => {
328+
setDraggingId(id);
329+
e.dataTransfer.effectAllowed = "move";
330+
};
331+
332+
const handleDragOver = (_e: React.DragEvent, id: AnnouncementCategory) => {
333+
setDragOverId(id);
334+
};
335+
336+
const handleDragEnd = () => {
337+
setDraggingId(null);
338+
setDragOverId(null);
339+
};
340+
341+
const handleDrop = (_e: React.DragEvent, targetId: AnnouncementCategory) => {
342+
if (!draggingId || draggingId === targetId) return;
343+
setTabs((prev) => {
344+
const updated = [...prev];
345+
const fromIndex = updated.findIndex((t) => t.id === draggingId);
346+
const toIndex = updated.findIndex((t) => t.id === targetId);
347+
if (fromIndex === -1 || toIndex === -1) return updated;
348+
const [moved] = updated.splice(fromIndex, 1);
349+
if (moved) {
350+
updated.splice(toIndex, 0, moved);
351+
}
352+
return updated;
353+
});
354+
setDraggingId(null);
355+
setDragOverId(null);
356+
};
357+
324358
const handleTabClick = (category: AnnouncementCategory) => {
325359
setActiveTab(category);
326360
setEditingCategory(null);
@@ -441,6 +475,12 @@ export const AnnouncementModal: React.FC<AnnouncementModalProps> = ({
441475
onClick={handleTabClick}
442476
onEdit={(id, label) => setEditingCategory({ id, label })}
443477
onDelete={(id) => setDeleteConfirm(id)}
478+
onDragStart={handleDragStart}
479+
onDragOver={handleDragOver}
480+
onDragEnd={handleDragEnd}
481+
onDrop={handleDrop}
482+
isDragging={draggingId === tab.id}
483+
isDragOver={dragOverId === tab.id}
444484
/>
445485
)}
446486
</div>

‎apps/codebility/app/home/announcements/AnnouncementTab.tsx‎

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client";
2-
3-
import React from "react";
4-
import { Pencil, Trash2 } from "lucide-react";
2+
import React, { useRef } from "react";
3+
import { Pencil, Trash2, GripVertical } from "lucide-react";
54
import { AnnouncementCategory } from "./types";
65

76
interface AnnouncementTabProps {
@@ -12,6 +11,12 @@ interface AnnouncementTabProps {
1211
onClick: (id: AnnouncementCategory) => void;
1312
onEdit?: (id: AnnouncementCategory, label: string) => void;
1413
onDelete?: (id: AnnouncementCategory) => void;
14+
onDragStart?: (e: React.DragEvent, id: AnnouncementCategory) => void;
15+
onDragOver?: (e: React.DragEvent, id: AnnouncementCategory) => void;
16+
onDragEnd?: () => void;
17+
onDrop?: (e: React.DragEvent, id: AnnouncementCategory) => void;
18+
isDragging?: boolean;
19+
isDragOver?: boolean;
1520
}
1621

1722
export const AnnouncementTab: React.FC<AnnouncementTabProps> = ({
@@ -22,21 +27,68 @@ export const AnnouncementTab: React.FC<AnnouncementTabProps> = ({
2227
onClick,
2328
onEdit,
2429
onDelete,
30+
onDragStart,
31+
onDragOver,
32+
onDragEnd,
33+
onDrop,
34+
isDragging = false,
35+
isDragOver = false,
2536
}) => {
37+
// Ref-based gate: synchronous, no re-render delay
38+
const gripPressed = useRef(false);
39+
2640
return (
2741
<div
42+
draggable={isAdmin}
43+
onDragStart={(e) => {
44+
if (!gripPressed.current) {
45+
e.preventDefault();
46+
return;
47+
}
48+
e.dataTransfer.setData("text/plain", id);
49+
e.dataTransfer.effectAllowed = "move";
50+
onDragStart?.(e, id);
51+
}}
52+
onDragOver={(e) => {
53+
e.preventDefault();
54+
e.dataTransfer.dropEffect = "move";
55+
onDragOver?.(e, id);
56+
}}
57+
onDragEnd={() => {
58+
gripPressed.current = false;
59+
onDragEnd?.();
60+
}}
61+
onDrop={(e) => {
62+
e.preventDefault();
63+
onDrop?.(e, id);
64+
}}
2865
className={`
2966
group relative flex items-center justify-between rounded-lg w-full
30-
${
31-
isActive
32-
? "bg-blue-500/10 text-blue-500 dark:bg-blue-500/20"
33-
: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800/50"
67+
transition-all duration-150 select-none
68+
${isActive
69+
? "bg-blue-500/10 text-blue-500 dark:bg-blue-500/20"
70+
: "text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800/50"
3471
}
72+
${isDragging ? "opacity-40 scale-[0.98]" : "opacity-100"}
73+
${isDragOver ? "border-t-2 border-blue-500" : "border-t-2 border-transparent"}
3574
`}
3675
>
76+
{/* Grip handle — only shown to admins on hover */}
77+
{isAdmin && (
78+
<span
79+
onMouseDown={() => { gripPressed.current = true; }}
80+
onMouseUp={() => { gripPressed.current = false; }}
81+
className="pl-2 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-150 cursor-grab active:cursor-grabbing text-gray-400 dark:text-gray-500"
82+
title="Drag to reorder"
83+
>
84+
<GripVertical className="h-4 w-4 pointer-events-none" />
85+
</span>
86+
)}
87+
3788
<button
89+
draggable={false}
3890
onClick={() => onClick(id)}
39-
className="flex-1 px-4 sm:px-6 py-3 sm:py-4 text-sm sm:text-base font-medium text-left truncate transition-all duration-200"
91+
className="flex-1 px-3 sm:px-4 py-3 sm:py-4 text-sm sm:text-base font-medium text-left truncate"
4092
role="tab"
4193
aria-selected={isActive}
4294
aria-controls={`panel-${id}`}
@@ -45,8 +97,9 @@ export const AnnouncementTab: React.FC<AnnouncementTabProps> = ({
4597
</button>
4698

4799
{isAdmin && (
48-
<div className="flex items-center gap-0.5 pr-2 opacity-0 group-hover:opacity-100 transition-opacity">
100+
<div className="flex items-center gap-0.5 pr-2 opacity-0 group-hover:opacity-100 transition-opacity duration-150">
49101
<button
102+
draggable={false}
50103
onClick={(e) => {
51104
e.stopPropagation();
52105
onEdit?.(id, label);
@@ -57,6 +110,7 @@ export const AnnouncementTab: React.FC<AnnouncementTabProps> = ({
57110
<Pencil className="h-3.5 w-3.5" />
58111
</button>
59112
<button
113+
draggable={false}
60114
onClick={(e) => {
61115
e.stopPropagation();
62116
onDelete?.(id);

0 commit comments

Comments
 (0)