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.
196 lines
6.8 KiB
TypeScript
196 lines
6.8 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, useCallback } from "react"
|
|
|
|
// Per-widget settings types
|
|
export interface RecentActivitySettings {
|
|
itemCount: number // 5, 10, 15
|
|
}
|
|
|
|
export interface TopProductsSettings {
|
|
itemCount: number // 3, 5, 10
|
|
showRevenue: boolean
|
|
}
|
|
|
|
export interface OverviewSettings {
|
|
showChange: boolean // Show % change from previous period
|
|
}
|
|
|
|
export interface RevenueChartSettings {
|
|
days: number // 7, 14, 30
|
|
showComparison: boolean
|
|
}
|
|
|
|
export interface LowStockSettings {
|
|
threshold: number // Show items with stock below this
|
|
itemCount: number
|
|
}
|
|
|
|
export interface RecentCustomersSettings {
|
|
itemCount: number
|
|
showSpent: boolean
|
|
}
|
|
|
|
export interface PendingChatsSettings {
|
|
showPreview: boolean
|
|
}
|
|
|
|
export type WidgetSettings =
|
|
| { type: "quick-actions" }
|
|
| { type: "overview"; settings: OverviewSettings }
|
|
| { type: "recent-activity"; settings: RecentActivitySettings }
|
|
| { type: "top-products"; settings: TopProductsSettings }
|
|
| { type: "revenue-chart"; settings: RevenueChartSettings }
|
|
| { type: "low-stock"; settings: LowStockSettings }
|
|
| { type: "recent-customers"; settings: RecentCustomersSettings }
|
|
| { type: "pending-chats"; settings: PendingChatsSettings }
|
|
|
|
export interface WidgetConfig {
|
|
id: string
|
|
title: string
|
|
visible: boolean
|
|
order: number
|
|
colSpan: number // 1, 2, 3, 4 (full)
|
|
settings?: Record<string, any>
|
|
}
|
|
|
|
const DEFAULT_WIDGETS: WidgetConfig[] = [
|
|
{ id: "quick-actions", title: "Quick Actions", visible: true, order: 0, colSpan: 4 },
|
|
{ id: "overview", title: "Overview", visible: true, order: 1, colSpan: 4, settings: { showChange: false } },
|
|
{ id: "recent-activity", title: "Recent Activity", visible: true, order: 2, colSpan: 2, settings: { itemCount: 10 } },
|
|
{ id: "top-products", title: "Top Products", visible: true, order: 3, colSpan: 2, settings: { itemCount: 5, showRevenue: true } },
|
|
{ id: "revenue-chart", title: "Revenue Chart", visible: false, order: 4, colSpan: 2, settings: { days: 7, showComparison: false } },
|
|
{ id: "low-stock", title: "Low Stock Alerts", visible: false, order: 5, colSpan: 2, settings: { threshold: 5, itemCount: 5 } },
|
|
{ id: "recent-customers", title: "Recent Customers", visible: false, order: 6, colSpan: 2, settings: { itemCount: 5, showSpent: true } },
|
|
{ id: "pending-chats", title: "Pending Chats", visible: false, order: 7, colSpan: 2, settings: { showPreview: true } },
|
|
]
|
|
|
|
const STORAGE_KEY = "dashboard-widget-layout-v3"
|
|
|
|
/**
|
|
* useWidgetLayout - Persist and manage dashboard widget visibility, order, and settings
|
|
*/
|
|
export function useWidgetLayout() {
|
|
const [widgets, setWidgets] = useState<WidgetConfig[]>(DEFAULT_WIDGETS)
|
|
const [isLoaded, setIsLoaded] = useState(false)
|
|
|
|
// Load from localStorage on mount
|
|
useEffect(() => {
|
|
if (typeof window === "undefined") return
|
|
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored) as WidgetConfig[]
|
|
// Merge with defaults to handle new widgets added in future
|
|
const merged = DEFAULT_WIDGETS.map(defaultWidget => {
|
|
const savedWidget = parsed.find(w => w.id === defaultWidget.id)
|
|
return savedWidget
|
|
? { ...defaultWidget, ...savedWidget, settings: { ...defaultWidget.settings, ...savedWidget.settings } }
|
|
: defaultWidget
|
|
})
|
|
setWidgets(merged.sort((a, b) => a.order - b.order))
|
|
}
|
|
} catch (e) {
|
|
console.warn("Failed to load widget layout:", e)
|
|
}
|
|
setIsLoaded(true)
|
|
}, [])
|
|
|
|
// Save to localStorage whenever widgets change
|
|
useEffect(() => {
|
|
if (!isLoaded || typeof window === "undefined") return
|
|
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(widgets))
|
|
} catch (e) {
|
|
console.warn("Failed to save widget layout:", e)
|
|
}
|
|
}, [widgets, isLoaded])
|
|
|
|
const toggleWidget = useCallback((widgetId: string) => {
|
|
setWidgets(prev =>
|
|
prev.map(w => w.id === widgetId ? { ...w, visible: !w.visible } : w)
|
|
)
|
|
}, [])
|
|
|
|
const moveWidget = useCallback((widgetId: string, direction: "up" | "down") => {
|
|
setWidgets(prev => {
|
|
const index = prev.findIndex(w => w.id === widgetId)
|
|
if (index === -1) return prev
|
|
|
|
const newIndex = direction === "up" ? index - 1 : index + 1
|
|
if (newIndex < 0 || newIndex >= prev.length) return prev
|
|
|
|
const newWidgets = [...prev]
|
|
const [widget] = newWidgets.splice(index, 1)
|
|
newWidgets.splice(newIndex, 0, widget)
|
|
|
|
// Update order values
|
|
return newWidgets.map((w, i) => ({ ...w, order: i }))
|
|
})
|
|
}, [])
|
|
|
|
const updateWidgetSettings = useCallback((widgetId: string, newSettings: Record<string, any>) => {
|
|
setWidgets(prev =>
|
|
prev.map(w => w.id === widgetId
|
|
? { ...w, settings: { ...w.settings, ...newSettings } }
|
|
: w
|
|
)
|
|
)
|
|
}, [])
|
|
|
|
const updateWidgetColSpan = useCallback((widgetId: string, colSpan: number) => {
|
|
setWidgets(prev =>
|
|
prev.map(w => w.id === widgetId ? { ...w, colSpan } : w)
|
|
)
|
|
}, [])
|
|
|
|
const getWidgetSettings = useCallback(<T extends Record<string, any>>(widgetId: string): T | undefined => {
|
|
return widgets.find(w => w.id === widgetId)?.settings as T | undefined
|
|
}, [widgets])
|
|
|
|
const resetLayout = useCallback(() => {
|
|
setWidgets(DEFAULT_WIDGETS)
|
|
}, [])
|
|
|
|
const getVisibleWidgets = useCallback(() => {
|
|
return widgets.filter(w => w.visible).sort((a, b) => a.order - b.order)
|
|
}, [widgets])
|
|
|
|
const isWidgetVisible = useCallback((widgetId: string) => {
|
|
return widgets.find(w => w.id === widgetId)?.visible ?? true
|
|
}, [widgets])
|
|
|
|
// Reorder widgets by moving activeId to overId's position
|
|
const reorderWidgets = useCallback((activeId: string, overId: string) => {
|
|
setWidgets(prev => {
|
|
const oldIndex = prev.findIndex(w => w.id === activeId)
|
|
const newIndex = prev.findIndex(w => w.id === overId)
|
|
|
|
if (oldIndex === -1 || newIndex === -1) return prev
|
|
|
|
const newWidgets = [...prev]
|
|
const [removed] = newWidgets.splice(oldIndex, 1)
|
|
newWidgets.splice(newIndex, 0, removed)
|
|
|
|
// Update order values
|
|
return newWidgets.map((w, i) => ({ ...w, order: i }))
|
|
})
|
|
}, [])
|
|
|
|
return {
|
|
widgets,
|
|
toggleWidget,
|
|
moveWidget,
|
|
reorderWidgets,
|
|
updateWidgetSettings,
|
|
updateWidgetColSpan,
|
|
getWidgetSettings,
|
|
resetLayout,
|
|
getVisibleWidgets,
|
|
isWidgetVisible,
|
|
isLoaded
|
|
}
|
|
}
|