'use client'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
DndContext,
closestCenter,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
type DragEndEvent,
} from '@dnd-kit/core'
import {
SortableContext,
rectSortingStrategy,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { GripVertical, X, Plus, RotateCcw, Check, LayoutGrid, Eye, EyeOff } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import {
getDefaultDashboardLayout,
resetDashboardLayout,
DASHBOARD_CATEGORY_ORDER,
DASHBOARD_WIDGET_META,
catalogByCategory,
hiddenWidgetIds,
isWidgetVisible,
normalizeDashboardLayout,
reorderWidgets,
setWidgetVisibility,
visibleWidgetsInZone,
type DashboardLayout,
type DashboardWidgetId,
type DashboardWidgetPlacement,
type DashboardWidgetZone,
} from '@/lib/dashboard/layout'
import { toast } from 'sonner'
interface DashboardWidgetGridProps {
renderWidget: (id: DashboardWidgetId) => React.ReactNode
}
function SortableWidget({
placement,
editMode,
onHide,
children,
}: {
placement: DashboardWidgetPlacement
editMode: boolean
onHide: (id: DashboardWidgetId) => void
children: React.ReactNode
}) {
const { t } = useLanguage()
const meta = DASHBOARD_WIDGET_META[placement.id]
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: placement.id, disabled: !editMode })
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 20 : undefined,
}
return (
{editMode && (
{!meta.required && (
)}
)}
{children}
)
}
function ZoneColumn({
zone,
placements,
editMode,
onHide,
renderWidget,
}: {
zone: DashboardWidgetZone
placements: DashboardWidgetPlacement[]
editMode: boolean
onHide: (id: DashboardWidgetId) => void
renderWidget: (id: DashboardWidgetId) => React.ReactNode
}) {
if (placements.length === 0) return null
return (
w.id)} strategy={verticalListSortingStrategy}>
{placements.map(placement => (
{renderWidget(placement.id)}
))}
)
}
export function DashboardWidgetGrid({ renderWidget }: DashboardWidgetGridProps) {
const { t } = useLanguage()
const [layout, setLayout] = useState(() => getDefaultDashboardLayout())
const [editMode, setEditMode] = useState(false)
const [catalogOpen, setCatalogOpen] = useState(false)
const [loaded, setLoaded] = useState(false)
const saveTimer = useRef | null>(null)
useEffect(() => {
fetch('/api/dashboard/layout', { cache: 'no-store' })
.then(r => r.ok ? r.json() : null)
.then(json => {
if (json?.layout) setLayout(normalizeDashboardLayout(json.layout))
})
.catch(() => {})
.finally(() => setLoaded(true))
}, [])
const persistLayout = useCallback((next: DashboardLayout, immediate = false) => {
if (saveTimer.current) clearTimeout(saveTimer.current)
const save = () => {
fetch('/api/dashboard/layout', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ layout: next }),
}).catch(() => {})
}
if (immediate) save()
else saveTimer.current = setTimeout(save, 600)
}, [])
const applyLayout = useCallback((next: DashboardLayout, immediate = false) => {
const normalized = normalizeDashboardLayout(next)
setLayout(normalized)
persistLayout(normalized, immediate)
}, [persistLayout])
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 180, tolerance: 8 } }),
)
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event
if (!over || active.id === over.id) return
applyLayout(reorderWidgets(layout, active.id as DashboardWidgetId, over.id as DashboardWidgetId))
}, [applyLayout, layout])
const handleHide = useCallback((id: DashboardWidgetId) => {
applyLayout(setWidgetVisibility(layout, id, false))
}, [applyLayout, layout])
const handleToggle = useCallback((id: DashboardWidgetId) => {
const visible = isWidgetVisible(layout, id)
if (visible) {
applyLayout(setWidgetVisibility(layout, id, false))
} else {
const maxOrder = Math.max(...layout.widgets.map(w => w.order), 0)
applyLayout({
...layout,
widgets: layout.widgets.map(w =>
w.id === id ? { ...w, visible: true, order: maxOrder + 1 } : w,
),
})
}
}, [applyLayout, layout])
const handleReset = useCallback(() => {
const fresh = resetDashboardLayout()
setLayout(fresh)
persistLayout(fresh, true)
setCatalogOpen(false)
toast.success(t('homeDashboard.widgetResetDone'))
}, [persistLayout, t])
const fullWidgets = visibleWidgetsInZone(layout, 'full')
const mainWidgets = visibleWidgetsInZone(layout, 'main')
const sideWidgets = visibleWidgetsInZone(layout, 'side')
const hidden = hiddenWidgetIds(layout)
const catalog = catalogByCategory(layout)
const hasVisibleWidgets = fullWidgets.length + mainWidgets.length + sideWidgets.length > 0
return (
<>
{loaded ? (
hasVisibleWidgets ? (
{fullWidgets.length > 0 && (
w.id)} strategy={rectSortingStrategy}>
{fullWidgets.map(placement => (
{renderWidget(placement.id)}
))}
)}
{(mainWidgets.length > 0 || sideWidgets.length > 0) && (
{mainWidgets.length > 0 && (
)}
{sideWidgets.length > 0 && (
)}
)}
) : (
{t('homeDashboard.layoutEmptyTitle')}
{t('homeDashboard.layoutEmptyHint')}
)
) : (
)}
{editMode ? (
<>
>
) : (
)}
{editMode && catalogOpen && (
{t('homeDashboard.widgetCatalogTitle')}
{DASHBOARD_CATEGORY_ORDER.map(category => {
const ids = catalog[category]
if (ids.length === 0) return null
return (
{t(`homeDashboard.widgetCategories.${category}`)}
{ids.map(id => {
const meta = DASHBOARD_WIDGET_META[id]
const active = isWidgetVisible(layout, id)
return (
)
})}
)
})}
)}
>
)
}