"use client";
import { useState } from "react";
import {
GripVertical,
RotateCcw,
Check,
Loader2,
ArrowUp,
ArrowDown,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
type Item = {
id: string;
label: string;
description?: string;
};
const defaultItems: Item[] = [
{ id: "1", label: "Dashboard", description: "Main overview" },
{ id: "2", label: "Projects", description: "Your work" },
{ id: "3", label: "Tasks", description: "To-do items" },
{ id: "4", label: "Calendar", description: "Schedule" },
{ id: "5", label: "Messages", description: "Communications" },
{ id: "6", label: "Settings", description: "Preferences" },
];
export const title = "React Dialog Block Reorder Items";
export default function DialogReorderItems() {
const [open, setOpen] = useState(false);
const [items, setItems] = useState<Item[]>(defaultItems);
const [originalItems] = useState<Item[]>(defaultItems);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
const [isSaving, setIsSaving] = useState(false);
const handleDragStart = (index: number) => {
setDraggedIndex(index);
};
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault();
if (draggedIndex === null || draggedIndex === index) return;
setDragOverIndex(index);
};
const handleDrop = (index: number) => {
if (draggedIndex === null || draggedIndex === index) return;
const newItems = [...items];
const [draggedItem] = newItems.splice(draggedIndex, 1);
newItems.splice(index, 0, draggedItem);
setItems(newItems);
setDraggedIndex(null);
setDragOverIndex(null);
};
const handleDragEnd = () => {
setDraggedIndex(null);
setDragOverIndex(null);
};
const moveItem = (index: number, direction: "up" | "down") => {
const newIndex = direction === "up" ? index - 1 : index + 1;
if (newIndex < 0 || newIndex >= items.length) return;
const newItems = [...items];
[newItems[index], newItems[newIndex]] = [newItems[newIndex], newItems[index]];
setItems(newItems);
};
const handleReset = () => {
setItems([...originalItems]);
};
const handleSave = async () => {
setIsSaving(true);
await new Promise((resolve) => setTimeout(resolve, 1000));
setIsSaving(false);
setOpen(false);
};
const hasChanges = JSON.stringify(items) !== JSON.stringify(originalItems);
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<GripVertical className="mr-2 h-4 w-4" />
Reorder Items
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<GripVertical className="h-5 w-5" />
Reorder Items
</DialogTitle>
<DialogDescription>
Drag and drop to reorder, or use the arrow buttons.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
{items.map((item, index) => (
<div
key={item.id}
draggable
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDrop={() => handleDrop(index)}
onDragEnd={handleDragEnd}
className={cn(
"flex items-center gap-3 rounded-lg border p-3 transition-all",
draggedIndex === index && "opacity-50",
dragOverIndex === index &&
draggedIndex !== null &&
"border-primary border-dashed"
)}
>
<div
className="cursor-grab text-muted-foreground hover:text-foreground active:cursor-grabbing"
aria-label="Drag to reorder"
>
<GripVertical className="h-5 w-5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{item.label}</p>
{item.description && (
<p className="text-xs text-muted-foreground truncate">
{item.description}
</p>
)}
</div>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => moveItem(index, "up")}
disabled={index === 0}
aria-label="Move up"
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => moveItem(index, "down")}
disabled={index === items.length - 1}
aria-label="Move down"
>
<ArrowDown className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
{hasChanges && (
<p className="mt-4 text-center text-sm text-muted-foreground">
You have unsaved changes
</p>
)}
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
variant="ghost"
onClick={handleReset}
disabled={!hasChanges || isSaving}
className="sm:mr-auto"
>
<RotateCcw className="mr-2 h-4 w-4" />
Reset
</Button>
<Button variant="outline" onClick={() => setOpen(false)} disabled={isSaving}>
Cancel
</Button>
<Button onClick={handleSave} disabled={!hasChanges || isSaving}>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>
<Check className="mr-2 h-4 w-4" />
Save Order
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}