Sortable Image Gallery
An image gallery uploader that allows reordering completed items using drag and drop.
Preview
Code
1import {2 UplofileRoot,3 UplofileDropzone,4 UplofileTrigger,5 UplofilePreview,6 type UploadFileItem,7} from "@/components/ui/uplofile";8import {9 IoAddOutline,10 IoImageOutline,11 IoReloadOutline,12 IoCheckmarkCircleOutline,13 IoAlertCircleOutline,14 IoReorderTwoOutline,15} from "react-icons/io5";16import { mockUpload } from "@/lib/utils.ts";17import {18 DndContext,19 closestCenter,20 KeyboardSensor,21 PointerSensor,22 useSensor,23 useSensors,24 type DragEndEvent,25} from "@dnd-kit/core";26import {27 arrayMove,28 SortableContext,29 sortableKeyboardCoordinates,30 rectSortingStrategy,31 useSortable,32} from "@dnd-kit/sortable";33import { CSS } from "@dnd-kit/utilities";3435export default function SortableGalleryDemo() {36 const sensors = useSensors(37 useSensor(PointerSensor),38 useSensor(KeyboardSensor, {39 coordinateGetter: sortableKeyboardCoordinates,40 }),41 );4243 return (44 <UplofileRoot upload={mockUpload} accept="image/*" multiple>45 <UplofilePreview46 render={({ items, setItems }) => {47 function handleDragEnd(event: DragEndEvent) {48 const { active, over } = event;4950 if (over && active.id !== over.id) {51 const oldIndex = items.findIndex(52 (item) => item.uid === active.id,53 );54 const newIndex = items.findIndex((item) => item.uid === over.id);5556 setItems(arrayMove(items, oldIndex, newIndex));57 }58 }5960 return (61 <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">62 <DndContext63 sensors={sensors}64 collisionDetection={closestCenter}65 onDragEnd={handleDragEnd}66 >67 <SortableContext68 items={items.map((i) => i.uid)}69 strategy={rectSortingStrategy}70 >71 {items.map((item) => (72 <SortableImageItem key={item.uid} item={item} />73 ))}74 </SortableContext>75 </DndContext>7677 <UplofileDropzone className="group aspect-square rounded-xl flex flex-col items-center justify-center border-2 border-dashed border-muted-foreground/25 hover:border-primary/50 hover:bg-primary/5 cursor-pointer transition-all duration-200 data-[dragging=true]:border-primary data-[dragging=true]:bg-primary/10 data-[dragging=true]:scale-95">78 <UplofileTrigger>79 <div className="flex flex-col items-center gap-2 text-muted-foreground group-hover:text-primary transition-colors">80 <div className="p-3 rounded-full bg-muted group-hover:bg-primary/10 transition-colors">81 <IoAddOutline className="h-6 w-6" />82 </div>83 <span className="text-[10px] font-bold uppercase tracking-wider">84 Add Image85 </span>86 </div>87 </UplofileTrigger>88 </UplofileDropzone>89 </div>90 );91 }}92 />93 </UplofileRoot>94 );95}9697function SortableImageItem({ item }: { item: UploadFileItem }) {98 const isDraggable = item.status === "done";99 const {100 attributes,101 listeners,102 setNodeRef,103 transform,104 transition,105 isDragging,106 } = useSortable({107 id: item.uid,108 disabled: !isDraggable,109 });110111 const style = {112 transform: CSS.Transform.toString(transform),113 transition,114 zIndex: isDragging ? 10 : undefined,115 };116117 return (118 <div119 ref={setNodeRef}120 style={style}121 className={`group relative aspect-square rounded-xl overflow-hidden bg-white border shadow-sm animate-in fade-in zoom-in-95 duration-200 ${isDragging ? "opacity-50" : ""}`}122 >123 {item.previewUrl ? (124 <img125 src={item.previewUrl}126 alt={item.name}127 className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"128 />129 ) : (130 <div className="w-full h-full flex items-center justify-center bg-muted/50">131 <IoImageOutline className="h-6 w-6 text-muted-foreground/40" />132 </div>133 )}134135 {item.status === "uploading" && (136 <div className="absolute inset-0 bg-black/60 backdrop-blur-[2px] flex flex-col items-center justify-center p-2">137 <IoReloadOutline className="h-6 w-6 text-white animate-spin mb-2" />138 <div className="w-full bg-white/20 rounded-full h-1 max-w-[40px]">139 <div140 className="bg-white h-full rounded-full transition-all duration-300"141 style={{ width: `${item.progress}%` }}142 />143 </div>144 </div>145 )}146147 {item.status === "done" && (148 <>149 <div className="absolute top-2 right-2 bg-emerald-500 text-white p-1 rounded-full shadow-lg scale-0 group-hover:scale-100 transition-transform duration-200">150 <IoCheckmarkCircleOutline className="h-3 w-3" />151 </div>152 {/* Drag Handle */}153 <div154 {...attributes}155 {...listeners}156 className="absolute top-2 left-2 bg-white/80 backdrop-blur-sm text-gray-600 p-1 rounded-md shadow-sm border opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing"157 >158 <IoReorderTwoOutline className="h-3 w-3" />159 </div>160 </>161 )}162163 {item.status === "error" && (164 <div className="absolute inset-0 bg-destructive/60 backdrop-blur-[1px] flex flex-col items-center justify-center p-2 text-white animate-in zoom-in-95">165 <IoAlertCircleOutline className="h-6 w-6 mb-1" />166 <span className="text-[10px] font-bold uppercase tracking-wider">167 Failed168 </span>169 </div>170 )}171172 <div className="absolute inset-0 bg-black/5 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />173 </div>174 );175}
Key Points
- → Integrated with
@dnd-kitfor smooth drag-and-drop - → Only completed items (
status === 'done') are draggable - → Drag handle appears on hover in the top-left corner
- → Reordering is preserved through the
setItemscallback