Add modular dashboard widgets and layout editor
Some checks failed
Build Frontend / build (push) Failing after 7s
Some checks failed
Build Frontend / build (push) Failing after 7s
Introduces a modular dashboard system with draggable, configurable widgets including revenue, low stock, recent customers, and pending chats. Adds a dashboard editor for layout customization, widget visibility, and settings. Refactors dashboard content to use the new widget system and improves UI consistency and interactivity.
This commit is contained in:
157
components/dashboard/dashboard-editor.tsx
Normal file
157
components/dashboard/dashboard-editor.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client"
|
||||
|
||||
import React, { useState } from "react"
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent,
|
||||
DragOverlay,
|
||||
DragStartEvent,
|
||||
} from "@dnd-kit/core"
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
rectSortingStrategy,
|
||||
arrayMove,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Edit3, X, Check, RotateCcw } from "lucide-react"
|
||||
import { WidgetConfig } from "@/hooks/useWidgetLayout"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
interface DashboardEditorProps {
|
||||
widgets: WidgetConfig[]
|
||||
isEditMode: boolean
|
||||
onToggleEditMode: () => void
|
||||
onReorder: (activeId: string, overId: string) => void
|
||||
onReset: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DashboardEditor({
|
||||
widgets,
|
||||
isEditMode,
|
||||
onToggleEditMode,
|
||||
onReorder,
|
||||
onReset,
|
||||
children
|
||||
}: DashboardEditorProps) {
|
||||
const [activeId, setActiveId] = useState<string | null>(null)
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
)
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
setActiveId(event.active.id as string)
|
||||
}
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
onReorder(active.id as string, over.id as string)
|
||||
}
|
||||
|
||||
setActiveId(null)
|
||||
}
|
||||
|
||||
const handleDragCancel = () => {
|
||||
setActiveId(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
<SortableContext
|
||||
items={widgets.map(w => w.id)}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
{children}
|
||||
</SortableContext>
|
||||
|
||||
{/* Edit Mode Banner */}
|
||||
<AnimatePresence>
|
||||
{isEditMode && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 50 }}
|
||||
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50"
|
||||
>
|
||||
<div className="flex items-center gap-3 bg-primary text-primary-foreground px-4 py-2.5 rounded-full shadow-lg border border-primary-foreground/20">
|
||||
<span className="text-sm font-medium">
|
||||
Editing Dashboard • Drag widgets to reorder
|
||||
</span>
|
||||
<div className="h-4 w-px bg-primary-foreground/30" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 hover:bg-primary-foreground/20"
|
||||
onClick={onReset}
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="h-7 px-3"
|
||||
onClick={onToggleEditMode}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5 mr-1" />
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</DndContext>
|
||||
)
|
||||
}
|
||||
|
||||
// Edit button component to add to the header
|
||||
export function EditDashboardButton({
|
||||
isEditMode,
|
||||
onToggle
|
||||
}: {
|
||||
isEditMode: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant={isEditMode ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 gap-2"
|
||||
onClick={onToggle}
|
||||
>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Cancel</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Edit3 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Edit Layout</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user