@moontra/moonui-pro 2.4.5 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +127 -25
- package/dist/index.mjs +2023 -186
- package/package.json +4 -1
- package/src/components/advanced-chart/index.tsx +11 -1
- package/src/components/dashboard/dashboard-grid.tsx +327 -0
- package/src/components/dashboard/demo.tsx +308 -0
- package/src/components/dashboard/index.tsx +683 -275
- package/src/components/dashboard/time-range-picker.tsx +267 -0
- package/src/components/dashboard/types.ts +222 -0
- package/src/components/dashboard/widgets/activity-feed.tsx +344 -0
- package/src/components/dashboard/widgets/chart-widget.tsx +418 -0
- package/src/components/dashboard/widgets/metric-card.tsx +343 -0
- package/src/components/index.ts +2 -2
- package/src/components/ui/calendar.tsx +65 -0
- package/src/components/ui/index.ts +12 -0
- package/src/components/ui/scroll-area.tsx +47 -0
- package/src/components/ui/sheet.tsx +139 -0
|
@@ -1,333 +1,741 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
|
-
import {
|
|
5
|
-
import { MoonUIBadgePro as Badge } from '../ui/badge'
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
6
5
|
import { Button } from '../ui/button'
|
|
6
|
+
import { Badge } from '../ui/badge'
|
|
7
|
+
import { Input } from '../ui/input'
|
|
8
|
+
import { ScrollArea } from '../ui/scroll-area'
|
|
9
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
|
|
7
10
|
import {
|
|
8
|
-
|
|
9
|
-
TrendingDown,
|
|
10
|
-
Users,
|
|
11
|
-
DollarSign,
|
|
12
|
-
Activity,
|
|
13
|
-
BarChart3,
|
|
14
|
-
Eye,
|
|
11
|
+
Activity,
|
|
15
12
|
Download,
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
Upload,
|
|
14
|
+
Settings,
|
|
15
|
+
Edit3,
|
|
16
|
+
Save,
|
|
17
|
+
X,
|
|
18
|
+
Plus,
|
|
19
|
+
RefreshCw,
|
|
20
|
+
Share2,
|
|
21
|
+
Filter,
|
|
22
|
+
LayoutGrid,
|
|
23
|
+
Palette,
|
|
24
|
+
Search,
|
|
25
|
+
Bell,
|
|
26
|
+
Menu,
|
|
27
|
+
ChevronRight,
|
|
28
|
+
Sparkles,
|
|
29
|
+
BarChart3,
|
|
30
|
+
Users,
|
|
31
|
+
DollarSign,
|
|
32
|
+
TrendingUp,
|
|
18
33
|
Calendar,
|
|
34
|
+
Map,
|
|
35
|
+
Table,
|
|
36
|
+
Clock,
|
|
37
|
+
Target,
|
|
19
38
|
ArrowUpRight,
|
|
20
|
-
|
|
21
|
-
Minus,
|
|
22
|
-
Lock,
|
|
23
|
-
Sparkles
|
|
39
|
+
CheckCircle
|
|
24
40
|
} from 'lucide-react'
|
|
25
41
|
import { cn } from '../../lib/utils'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
42
|
+
import { DashboardGrid } from './dashboard-grid'
|
|
43
|
+
import { TimeRangePicker } from './time-range-picker'
|
|
44
|
+
import { MetricCard } from './widgets/metric-card'
|
|
45
|
+
import { ChartWidget } from './widgets/chart-widget'
|
|
46
|
+
import { ActivityFeed } from './widgets/activity-feed'
|
|
47
|
+
import {
|
|
48
|
+
Widget,
|
|
49
|
+
DashboardConfig,
|
|
50
|
+
DashboardTheme,
|
|
51
|
+
TimeRange,
|
|
52
|
+
MetricData,
|
|
53
|
+
ChartData,
|
|
54
|
+
ActivityItem,
|
|
55
|
+
DashboardTemplate,
|
|
56
|
+
WidgetType
|
|
57
|
+
} from './types'
|
|
58
|
+
import {
|
|
59
|
+
DropdownMenu,
|
|
60
|
+
DropdownMenuContent,
|
|
61
|
+
DropdownMenuItem,
|
|
62
|
+
DropdownMenuSeparator,
|
|
63
|
+
DropdownMenuSub,
|
|
64
|
+
DropdownMenuSubContent,
|
|
65
|
+
DropdownMenuSubTrigger,
|
|
66
|
+
DropdownMenuTrigger,
|
|
67
|
+
} from '../ui/dropdown-menu'
|
|
68
|
+
import {
|
|
69
|
+
Dialog,
|
|
70
|
+
DialogContent,
|
|
71
|
+
DialogDescription,
|
|
72
|
+
DialogHeader,
|
|
73
|
+
DialogTitle,
|
|
74
|
+
DialogTrigger,
|
|
75
|
+
} from '../ui/dialog'
|
|
76
|
+
import {
|
|
77
|
+
Sheet,
|
|
78
|
+
SheetContent,
|
|
79
|
+
SheetDescription,
|
|
80
|
+
SheetHeader,
|
|
81
|
+
SheetTitle,
|
|
82
|
+
SheetTrigger,
|
|
83
|
+
} from '../ui/sheet'
|
|
51
84
|
|
|
52
85
|
interface DashboardProps {
|
|
53
|
-
|
|
54
|
-
widgets?:
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
config?: DashboardConfig
|
|
87
|
+
widgets?: Widget[]
|
|
88
|
+
templates?: DashboardTemplate[]
|
|
89
|
+
onConfigChange?: (config: DashboardConfig) => void
|
|
90
|
+
onWidgetAdd?: (widget: Omit<Widget, 'id'>) => void
|
|
91
|
+
onWidgetRemove?: (widgetId: string) => void
|
|
92
|
+
onWidgetUpdate?: (widgetId: string, updates: Partial<Widget>) => void
|
|
93
|
+
onExport?: (format: 'json' | 'pdf' | 'png') => void
|
|
94
|
+
onImport?: (file: File) => void
|
|
57
95
|
className?: string
|
|
58
96
|
showHeader?: boolean
|
|
59
97
|
title?: string
|
|
60
98
|
description?: string
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
99
|
+
editable?: boolean
|
|
100
|
+
realtime?: boolean
|
|
101
|
+
glassmorphism?: boolean
|
|
64
102
|
}
|
|
65
103
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
104
|
+
// Dashboard template'leri
|
|
105
|
+
const DASHBOARD_TEMPLATES: DashboardTemplate[] = [
|
|
106
|
+
{
|
|
107
|
+
id: 'analytics',
|
|
108
|
+
name: 'Analytics Dashboard',
|
|
109
|
+
description: 'Comprehensive analytics with metrics and charts',
|
|
110
|
+
category: 'analytics',
|
|
111
|
+
theme: 'analytics',
|
|
112
|
+
widgets: [
|
|
113
|
+
{
|
|
114
|
+
type: 'metric',
|
|
115
|
+
title: 'Total Revenue',
|
|
116
|
+
size: { w: 3, h: 2 },
|
|
117
|
+
position: { x: 0, y: 0 },
|
|
118
|
+
data: {
|
|
119
|
+
id: 'revenue',
|
|
120
|
+
title: 'Total Revenue',
|
|
121
|
+
value: 125430,
|
|
122
|
+
change: { value: 12, type: 'increase', period: 'last month' },
|
|
123
|
+
color: 'success',
|
|
124
|
+
icon: <DollarSign className="h-4 w-4" />,
|
|
125
|
+
sparkline: [100, 120, 115, 125, 130, 128, 132],
|
|
126
|
+
unit: '$'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'metric',
|
|
131
|
+
title: 'Active Users',
|
|
132
|
+
size: { w: 3, h: 2 },
|
|
133
|
+
position: { x: 3, y: 0 },
|
|
134
|
+
data: {
|
|
135
|
+
id: 'users',
|
|
136
|
+
title: 'Active Users',
|
|
137
|
+
value: 2543,
|
|
138
|
+
change: { value: 8, type: 'increase', period: 'last week' },
|
|
139
|
+
color: 'primary',
|
|
140
|
+
icon: <Users className="h-4 w-4" />,
|
|
141
|
+
sparkline: [200, 220, 210, 230, 225, 240, 254]
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: 'chart',
|
|
146
|
+
title: 'Revenue Trend',
|
|
147
|
+
size: { w: 6, h: 4 },
|
|
148
|
+
position: { x: 0, y: 2 },
|
|
149
|
+
data: {
|
|
150
|
+
type: 'area',
|
|
151
|
+
data: [
|
|
152
|
+
{ name: 'Jan', revenue: 4000, profit: 2400 },
|
|
153
|
+
{ name: 'Feb', revenue: 3000, profit: 1398 },
|
|
154
|
+
{ name: 'Mar', revenue: 5000, profit: 3200 },
|
|
155
|
+
{ name: 'Apr', revenue: 4500, profit: 2900 },
|
|
156
|
+
{ name: 'May', revenue: 6000, profit: 3800 },
|
|
157
|
+
{ name: 'Jun', revenue: 5500, profit: 3400 }
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
type: 'activity',
|
|
163
|
+
title: 'Recent Activity',
|
|
164
|
+
size: { w: 3, h: 6 },
|
|
165
|
+
position: { x: 9, y: 0 },
|
|
166
|
+
data: {
|
|
167
|
+
realtime: true,
|
|
168
|
+
items: [
|
|
169
|
+
{
|
|
170
|
+
id: '1',
|
|
171
|
+
type: 'success',
|
|
172
|
+
title: 'completed a purchase',
|
|
173
|
+
description: 'Order #12345 - $250.00',
|
|
174
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 5),
|
|
175
|
+
user: { name: 'John Doe' }
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: '2',
|
|
179
|
+
type: 'info',
|
|
180
|
+
title: 'signed up',
|
|
181
|
+
description: 'New user registration',
|
|
182
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 15),
|
|
183
|
+
user: { name: 'Jane Smith' }
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'sales',
|
|
192
|
+
name: 'Sales Dashboard',
|
|
193
|
+
description: 'Track sales performance and targets',
|
|
194
|
+
category: 'sales',
|
|
195
|
+
theme: 'sales',
|
|
196
|
+
widgets: [
|
|
197
|
+
// Sales-specific widgets
|
|
198
|
+
]
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'monitoring',
|
|
202
|
+
name: 'System Monitoring',
|
|
203
|
+
description: 'Monitor system health and performance',
|
|
204
|
+
category: 'operations',
|
|
205
|
+
theme: 'monitoring',
|
|
206
|
+
widgets: [
|
|
207
|
+
// Monitoring widgets
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
// Widget türleri için ikonlar
|
|
213
|
+
const WIDGET_TYPE_ICONS: Record<WidgetType, React.ReactNode> = {
|
|
214
|
+
metric: <BarChart3 className="h-4 w-4" />,
|
|
215
|
+
chart: <Activity className="h-4 w-4" />,
|
|
216
|
+
table: <Table className="h-4 w-4" />,
|
|
217
|
+
map: <Map className="h-4 w-4" />,
|
|
218
|
+
activity: <Clock className="h-4 w-4" />,
|
|
219
|
+
calendar: <Calendar className="h-4 w-4" />,
|
|
220
|
+
progress: <Target className="h-4 w-4" />,
|
|
221
|
+
comparison: <TrendingUp className="h-4 w-4" />
|
|
72
222
|
}
|
|
73
223
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
224
|
+
// Tema renkleri
|
|
225
|
+
const THEME_COLORS: Record<DashboardTheme, string> = {
|
|
226
|
+
analytics: 'from-blue-500/10 to-purple-500/10',
|
|
227
|
+
sales: 'from-green-500/10 to-emerald-500/10',
|
|
228
|
+
monitoring: 'from-orange-500/10 to-red-500/10',
|
|
229
|
+
finance: 'from-indigo-500/10 to-blue-500/10',
|
|
230
|
+
custom: 'from-gray-500/10 to-gray-600/10'
|
|
79
231
|
}
|
|
80
232
|
|
|
81
233
|
export function Dashboard({
|
|
82
|
-
|
|
83
|
-
widgets = [],
|
|
84
|
-
|
|
85
|
-
|
|
234
|
+
config,
|
|
235
|
+
widgets: initialWidgets = [],
|
|
236
|
+
templates = DASHBOARD_TEMPLATES,
|
|
237
|
+
onConfigChange,
|
|
238
|
+
onWidgetAdd,
|
|
239
|
+
onWidgetRemove,
|
|
240
|
+
onWidgetUpdate,
|
|
241
|
+
onExport,
|
|
242
|
+
onImport,
|
|
86
243
|
className,
|
|
87
244
|
showHeader = true,
|
|
88
245
|
title = 'Dashboard',
|
|
89
|
-
description = '
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
246
|
+
description = 'Real-time analytics and insights',
|
|
247
|
+
editable = true,
|
|
248
|
+
realtime = false,
|
|
249
|
+
glassmorphism = true
|
|
93
250
|
}: DashboardProps) {
|
|
251
|
+
// State yönetimi
|
|
252
|
+
const [editMode, setEditMode] = React.useState(false)
|
|
253
|
+
const [widgets, setWidgets] = React.useState<Widget[]>(initialWidgets)
|
|
254
|
+
const [selectedTheme, setSelectedTheme] = React.useState<DashboardTheme>('analytics')
|
|
255
|
+
const [timeRange, setTimeRange] = React.useState<TimeRange>()
|
|
256
|
+
const [searchQuery, setSearchQuery] = React.useState('')
|
|
257
|
+
const [showWidgetLibrary, setShowWidgetLibrary] = React.useState(false)
|
|
258
|
+
const [showTemplates, setShowTemplates] = React.useState(false)
|
|
94
259
|
const [refreshing, setRefreshing] = React.useState(false)
|
|
260
|
+
const [notifications, setNotifications] = React.useState<number>(3)
|
|
95
261
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
await onRefresh()
|
|
101
|
-
} finally {
|
|
102
|
-
setRefreshing(false)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
262
|
+
// WebSocket bağlantısı (real-time için)
|
|
263
|
+
React.useEffect(() => {
|
|
264
|
+
if (!realtime) return
|
|
106
265
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
266
|
+
// Simüle edilmiş WebSocket bağlantısı
|
|
267
|
+
const interval = setInterval(() => {
|
|
268
|
+
// Widget'ları güncelle
|
|
269
|
+
setWidgets(prev => prev.map(widget => {
|
|
270
|
+
if (widget.type === 'metric' && widget.data) {
|
|
271
|
+
const newValue = typeof widget.data.value === 'number'
|
|
272
|
+
? widget.data.value + Math.floor(Math.random() * 10 - 5)
|
|
273
|
+
: widget.data.value
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
...widget,
|
|
277
|
+
data: {
|
|
278
|
+
...widget.data,
|
|
279
|
+
value: newValue,
|
|
280
|
+
lastUpdated: new Date()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return widget
|
|
285
|
+
}))
|
|
286
|
+
}, 5000)
|
|
287
|
+
|
|
288
|
+
return () => clearInterval(interval)
|
|
289
|
+
}, [realtime])
|
|
290
|
+
|
|
291
|
+
// Widget ekle
|
|
292
|
+
const handleAddWidget = (type: WidgetType) => {
|
|
293
|
+
const newWidget: Widget = {
|
|
294
|
+
id: `widget-${Date.now()}`,
|
|
295
|
+
type,
|
|
296
|
+
title: `New ${type} Widget`,
|
|
297
|
+
size: { w: 3, h: 3 },
|
|
298
|
+
position: { x: 0, y: 0 },
|
|
299
|
+
data: generateDefaultData(type)
|
|
115
300
|
}
|
|
116
|
-
|
|
301
|
+
|
|
302
|
+
setWidgets(prev => [...prev, newWidget])
|
|
303
|
+
onWidgetAdd?.(newWidget)
|
|
304
|
+
setShowWidgetLibrary(false)
|
|
117
305
|
}
|
|
118
306
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
307
|
+
// Template uygula
|
|
308
|
+
const applyTemplate = (template: DashboardTemplate) => {
|
|
309
|
+
const newWidgets = template.widgets.map((w, index) => ({
|
|
310
|
+
...w,
|
|
311
|
+
id: `widget-${Date.now()}-${index}`
|
|
312
|
+
}))
|
|
313
|
+
|
|
314
|
+
setWidgets(newWidgets as Widget[])
|
|
315
|
+
setSelectedTheme(template.theme)
|
|
316
|
+
setShowTemplates(false)
|
|
128
317
|
}
|
|
129
318
|
|
|
130
|
-
|
|
319
|
+
// Varsayılan widget verisi oluştur
|
|
320
|
+
const generateDefaultData = (type: WidgetType): any => {
|
|
131
321
|
switch (type) {
|
|
132
|
-
case '
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
322
|
+
case 'metric':
|
|
323
|
+
return {
|
|
324
|
+
id: 'new-metric',
|
|
325
|
+
title: 'New Metric',
|
|
326
|
+
value: 0,
|
|
327
|
+
icon: <BarChart3 className="h-4 w-4" />,
|
|
328
|
+
color: 'primary'
|
|
329
|
+
}
|
|
330
|
+
case 'chart':
|
|
331
|
+
return {
|
|
332
|
+
type: 'line',
|
|
333
|
+
data: [
|
|
334
|
+
{ name: 'Mon', value: 100 },
|
|
335
|
+
{ name: 'Tue', value: 120 },
|
|
336
|
+
{ name: 'Wed', value: 110 },
|
|
337
|
+
{ name: 'Thu', value: 130 },
|
|
338
|
+
{ name: 'Fri', value: 125 }
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
case 'activity':
|
|
342
|
+
return {
|
|
343
|
+
items: [],
|
|
344
|
+
realtime: true
|
|
345
|
+
}
|
|
346
|
+
default:
|
|
347
|
+
return {}
|
|
138
348
|
}
|
|
139
349
|
}
|
|
140
350
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
351
|
+
// Widget kütüphanesi
|
|
352
|
+
const WidgetLibrary = () => (
|
|
353
|
+
<Sheet open={showWidgetLibrary} onOpenChange={setShowWidgetLibrary}>
|
|
354
|
+
<SheetContent className={cn(
|
|
355
|
+
"w-[400px] sm:w-[540px]",
|
|
356
|
+
glassmorphism && "bg-background/95 backdrop-blur-md"
|
|
357
|
+
)}>
|
|
358
|
+
<SheetHeader>
|
|
359
|
+
<SheetTitle>Widget Library</SheetTitle>
|
|
360
|
+
<SheetDescription>
|
|
361
|
+
Add widgets to customize your dashboard
|
|
362
|
+
</SheetDescription>
|
|
363
|
+
</SheetHeader>
|
|
364
|
+
|
|
365
|
+
<div className="mt-6 space-y-4">
|
|
366
|
+
<div className="relative">
|
|
367
|
+
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
|
368
|
+
<Input
|
|
369
|
+
placeholder="Search widgets..."
|
|
370
|
+
value={searchQuery}
|
|
371
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
372
|
+
className="pl-10"
|
|
373
|
+
/>
|
|
159
374
|
</div>
|
|
160
|
-
</CardHeader>
|
|
161
|
-
<CardContent>
|
|
162
|
-
<div className="text-2xl font-bold">{formatValue(metric.value)}</div>
|
|
163
|
-
{metric.change && (
|
|
164
|
-
<div className={cn(
|
|
165
|
-
"flex items-center text-xs mt-1",
|
|
166
|
-
getChangeColor(metric.change.type)
|
|
167
|
-
)}>
|
|
168
|
-
{getChangeIcon(metric.change.type)}
|
|
169
|
-
<span className="ml-1">
|
|
170
|
-
{Math.abs(metric.change.value)}% from {metric.change.period}
|
|
171
|
-
</span>
|
|
172
|
-
</div>
|
|
173
|
-
)}
|
|
174
|
-
{metric.description && (
|
|
175
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
176
|
-
{metric.description}
|
|
177
|
-
</p>
|
|
178
|
-
)}
|
|
179
|
-
</CardContent>
|
|
180
|
-
</Card>
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
375
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
</div>
|
|
207
|
-
</CardHeader>
|
|
208
|
-
<CardContent>
|
|
209
|
-
{widget.loading ? (
|
|
210
|
-
<div className="flex items-center justify-center h-32">
|
|
211
|
-
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
376
|
+
<ScrollArea className="h-[calc(100vh-200px)]">
|
|
377
|
+
<div className="grid grid-cols-2 gap-4">
|
|
378
|
+
{Object.entries(WIDGET_TYPE_ICONS)
|
|
379
|
+
.filter(([type]) => type.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
380
|
+
.map(([type, icon]) => (
|
|
381
|
+
<motion.div
|
|
382
|
+
key={type}
|
|
383
|
+
whileHover={{ scale: 1.02 }}
|
|
384
|
+
whileTap={{ scale: 0.98 }}
|
|
385
|
+
>
|
|
386
|
+
<Button
|
|
387
|
+
variant="outline"
|
|
388
|
+
className="w-full h-24 flex-col gap-2"
|
|
389
|
+
onClick={() => handleAddWidget(type as WidgetType)}
|
|
390
|
+
>
|
|
391
|
+
<div className="p-2 rounded-lg bg-muted">
|
|
392
|
+
{icon}
|
|
393
|
+
</div>
|
|
394
|
+
<span className="capitalize text-sm">{type}</span>
|
|
395
|
+
</Button>
|
|
396
|
+
</motion.div>
|
|
397
|
+
))}
|
|
212
398
|
</div>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
widget.content
|
|
219
|
-
)}
|
|
220
|
-
</CardContent>
|
|
221
|
-
</Card>
|
|
222
|
-
)
|
|
223
|
-
}
|
|
399
|
+
</ScrollArea>
|
|
400
|
+
</div>
|
|
401
|
+
</SheetContent>
|
|
402
|
+
</Sheet>
|
|
403
|
+
)
|
|
224
404
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
change: { value: 8, type: 'increase', period: 'last month' },
|
|
239
|
-
icon: <DollarSign className="h-4 w-4" />,
|
|
240
|
-
color: 'success'
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
id: 'active-sessions',
|
|
244
|
-
title: 'Active Sessions',
|
|
245
|
-
value: 1234,
|
|
246
|
-
change: { value: 2, type: 'decrease', period: 'last hour' },
|
|
247
|
-
icon: <Activity className="h-4 w-4" />,
|
|
248
|
-
color: 'warning'
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
id: 'conversion-rate',
|
|
252
|
-
title: 'Conversion Rate',
|
|
253
|
-
value: '3.2%',
|
|
254
|
-
change: { value: 0.3, type: 'increase', period: 'last week' },
|
|
255
|
-
icon: <TrendingUp className="h-4 w-4" />,
|
|
256
|
-
color: 'info'
|
|
257
|
-
}
|
|
258
|
-
]
|
|
405
|
+
// Template galerisi
|
|
406
|
+
const TemplateGallery = () => (
|
|
407
|
+
<Dialog open={showTemplates} onOpenChange={setShowTemplates}>
|
|
408
|
+
<DialogContent className={cn(
|
|
409
|
+
"max-w-4xl",
|
|
410
|
+
glassmorphism && "bg-background/95 backdrop-blur-md"
|
|
411
|
+
)}>
|
|
412
|
+
<DialogHeader>
|
|
413
|
+
<DialogTitle>Dashboard Templates</DialogTitle>
|
|
414
|
+
<DialogDescription>
|
|
415
|
+
Start with a pre-built template or create your own
|
|
416
|
+
</DialogDescription>
|
|
417
|
+
</DialogHeader>
|
|
259
418
|
|
|
260
|
-
|
|
419
|
+
<Tabs defaultValue="all" className="mt-4">
|
|
420
|
+
<TabsList>
|
|
421
|
+
<TabsTrigger value="all">All Templates</TabsTrigger>
|
|
422
|
+
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
|
423
|
+
<TabsTrigger value="sales">Sales</TabsTrigger>
|
|
424
|
+
<TabsTrigger value="operations">Operations</TabsTrigger>
|
|
425
|
+
</TabsList>
|
|
261
426
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
427
|
+
<TabsContent value="all" className="mt-4">
|
|
428
|
+
<div className="grid grid-cols-3 gap-4">
|
|
429
|
+
{templates.map((template) => (
|
|
430
|
+
<motion.div
|
|
431
|
+
key={template.id}
|
|
432
|
+
whileHover={{ y: -4 }}
|
|
433
|
+
className="cursor-pointer"
|
|
434
|
+
onClick={() => applyTemplate(template)}
|
|
435
|
+
>
|
|
436
|
+
<div className={cn(
|
|
437
|
+
"relative overflow-hidden rounded-lg border p-4 hover:shadow-lg transition-all",
|
|
438
|
+
"bg-gradient-to-br", THEME_COLORS[template.theme]
|
|
439
|
+
)}>
|
|
440
|
+
{template.isPro && (
|
|
441
|
+
<Badge className="absolute top-2 right-2" variant="secondary">
|
|
442
|
+
PRO
|
|
443
|
+
</Badge>
|
|
444
|
+
)}
|
|
445
|
+
<h3 className="font-semibold mb-1">{template.name}</h3>
|
|
446
|
+
<p className="text-sm text-muted-foreground">
|
|
447
|
+
{template.description}
|
|
448
|
+
</p>
|
|
449
|
+
<div className="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
|
|
450
|
+
<span>{template.widgets.length} widgets</span>
|
|
451
|
+
<span>•</span>
|
|
452
|
+
<span className="capitalize">{template.category}</span>
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
</motion.div>
|
|
456
|
+
))}
|
|
457
|
+
</div>
|
|
458
|
+
</TabsContent>
|
|
459
|
+
</Tabs>
|
|
460
|
+
</DialogContent>
|
|
461
|
+
</Dialog>
|
|
462
|
+
)
|
|
276
463
|
|
|
277
464
|
return (
|
|
278
|
-
<div
|
|
465
|
+
<motion.div
|
|
466
|
+
className={cn(
|
|
467
|
+
"w-full min-h-screen",
|
|
468
|
+
glassmorphism && "bg-gradient-to-br",
|
|
469
|
+
glassmorphism && THEME_COLORS[selectedTheme],
|
|
470
|
+
className
|
|
471
|
+
)}
|
|
472
|
+
initial={{ opacity: 0 }}
|
|
473
|
+
animate={{ opacity: 1 }}
|
|
474
|
+
transition={{ duration: 0.5 }}
|
|
475
|
+
>
|
|
279
476
|
{/* Header */}
|
|
280
477
|
{showHeader && (
|
|
281
|
-
<div
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
</div>
|
|
286
|
-
{refreshable && (
|
|
287
|
-
<Button
|
|
288
|
-
variant="outline"
|
|
289
|
-
onClick={handleRefresh}
|
|
290
|
-
disabled={refreshing}
|
|
291
|
-
>
|
|
292
|
-
{refreshing ? (
|
|
293
|
-
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div>
|
|
294
|
-
) : (
|
|
295
|
-
<Activity className="h-4 w-4 mr-2" />
|
|
296
|
-
)}
|
|
297
|
-
Refresh
|
|
298
|
-
</Button>
|
|
478
|
+
<motion.div
|
|
479
|
+
className={cn(
|
|
480
|
+
"sticky top-0 z-30 border-b",
|
|
481
|
+
glassmorphism ? "bg-background/80 backdrop-blur-md" : "bg-background"
|
|
299
482
|
)}
|
|
300
|
-
|
|
301
|
-
|
|
483
|
+
initial={{ y: -50 }}
|
|
484
|
+
animate={{ y: 0 }}
|
|
485
|
+
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
|
486
|
+
>
|
|
487
|
+
<div className="px-6 py-4">
|
|
488
|
+
<div className="flex items-center justify-between">
|
|
489
|
+
<div className="flex items-center gap-4">
|
|
490
|
+
<motion.div
|
|
491
|
+
whileHover={{ scale: 1.05 }}
|
|
492
|
+
whileTap={{ scale: 0.95 }}
|
|
493
|
+
>
|
|
494
|
+
<Button variant="ghost" size="sm" className="lg:hidden">
|
|
495
|
+
<Menu className="h-5 w-5" />
|
|
496
|
+
</Button>
|
|
497
|
+
</motion.div>
|
|
498
|
+
|
|
499
|
+
<div>
|
|
500
|
+
<div className="flex items-center gap-2">
|
|
501
|
+
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
|
502
|
+
{realtime && (
|
|
503
|
+
<Badge variant="secondary" className="animate-pulse">
|
|
504
|
+
<span className="mr-1 h-1.5 w-1.5 rounded-full bg-green-500" />
|
|
505
|
+
Real-time
|
|
506
|
+
</Badge>
|
|
507
|
+
)}
|
|
508
|
+
</div>
|
|
509
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
302
512
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
513
|
+
{/* Header actions */}
|
|
514
|
+
<div className="flex items-center gap-2">
|
|
515
|
+
{/* Time range picker */}
|
|
516
|
+
<TimeRangePicker
|
|
517
|
+
value={timeRange}
|
|
518
|
+
onChange={setTimeRange}
|
|
519
|
+
glassmorphism={glassmorphism}
|
|
520
|
+
/>
|
|
307
521
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
522
|
+
{/* Notifications */}
|
|
523
|
+
<div className="relative">
|
|
524
|
+
<Button variant="ghost" size="sm" className="relative">
|
|
525
|
+
<Bell className="h-4 w-4" />
|
|
526
|
+
{notifications > 0 && (
|
|
527
|
+
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground flex items-center justify-center">
|
|
528
|
+
{notifications}
|
|
529
|
+
</span>
|
|
530
|
+
)}
|
|
531
|
+
</Button>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
{/* Theme selector */}
|
|
535
|
+
<DropdownMenu>
|
|
536
|
+
<DropdownMenuTrigger asChild>
|
|
537
|
+
<Button variant="ghost" size="sm">
|
|
538
|
+
<Palette className="h-4 w-4" />
|
|
539
|
+
</Button>
|
|
540
|
+
</DropdownMenuTrigger>
|
|
541
|
+
<DropdownMenuContent align="end">
|
|
542
|
+
<DropdownMenuItem onClick={() => setSelectedTheme('analytics')}>
|
|
543
|
+
<div className="flex items-center gap-2">
|
|
544
|
+
<div className="h-4 w-4 rounded bg-gradient-to-br from-blue-500 to-purple-500" />
|
|
545
|
+
Analytics
|
|
546
|
+
</div>
|
|
547
|
+
</DropdownMenuItem>
|
|
548
|
+
<DropdownMenuItem onClick={() => setSelectedTheme('sales')}>
|
|
549
|
+
<div className="flex items-center gap-2">
|
|
550
|
+
<div className="h-4 w-4 rounded bg-gradient-to-br from-green-500 to-emerald-500" />
|
|
551
|
+
Sales
|
|
552
|
+
</div>
|
|
553
|
+
</DropdownMenuItem>
|
|
554
|
+
<DropdownMenuItem onClick={() => setSelectedTheme('monitoring')}>
|
|
555
|
+
<div className="flex items-center gap-2">
|
|
556
|
+
<div className="h-4 w-4 rounded bg-gradient-to-br from-orange-500 to-red-500" />
|
|
557
|
+
Monitoring
|
|
558
|
+
</div>
|
|
559
|
+
</DropdownMenuItem>
|
|
560
|
+
</DropdownMenuContent>
|
|
561
|
+
</DropdownMenu>
|
|
562
|
+
|
|
563
|
+
{/* More actions */}
|
|
564
|
+
<DropdownMenu>
|
|
565
|
+
<DropdownMenuTrigger asChild>
|
|
566
|
+
<Button variant="outline" size="sm">
|
|
567
|
+
<Settings className="h-4 w-4 mr-2" />
|
|
568
|
+
Actions
|
|
569
|
+
</Button>
|
|
570
|
+
</DropdownMenuTrigger>
|
|
571
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
572
|
+
{editable && (
|
|
573
|
+
<>
|
|
574
|
+
<DropdownMenuItem onClick={() => setEditMode(!editMode)}>
|
|
575
|
+
<Edit3 className="mr-2 h-4 w-4" />
|
|
576
|
+
{editMode ? 'Exit Edit Mode' : 'Edit Layout'}
|
|
577
|
+
</DropdownMenuItem>
|
|
578
|
+
<DropdownMenuItem onClick={() => setShowWidgetLibrary(true)}>
|
|
579
|
+
<Plus className="mr-2 h-4 w-4" />
|
|
580
|
+
Add Widget
|
|
581
|
+
</DropdownMenuItem>
|
|
582
|
+
<DropdownMenuItem onClick={() => setShowTemplates(true)}>
|
|
583
|
+
<LayoutGrid className="mr-2 h-4 w-4" />
|
|
584
|
+
Templates
|
|
585
|
+
</DropdownMenuItem>
|
|
586
|
+
<DropdownMenuSeparator />
|
|
587
|
+
</>
|
|
588
|
+
)}
|
|
589
|
+
|
|
590
|
+
<DropdownMenuSub>
|
|
591
|
+
<DropdownMenuSubTrigger>
|
|
592
|
+
<Download className="mr-2 h-4 w-4" />
|
|
593
|
+
Export
|
|
594
|
+
</DropdownMenuSubTrigger>
|
|
595
|
+
<DropdownMenuSubContent>
|
|
596
|
+
<DropdownMenuItem onClick={() => onExport?.('json')}>
|
|
597
|
+
As JSON
|
|
598
|
+
</DropdownMenuItem>
|
|
599
|
+
<DropdownMenuItem onClick={() => onExport?.('pdf')}>
|
|
600
|
+
As PDF
|
|
601
|
+
</DropdownMenuItem>
|
|
602
|
+
<DropdownMenuItem onClick={() => onExport?.('png')}>
|
|
603
|
+
As Image
|
|
604
|
+
</DropdownMenuItem>
|
|
605
|
+
</DropdownMenuSubContent>
|
|
606
|
+
</DropdownMenuSub>
|
|
607
|
+
|
|
608
|
+
<DropdownMenuItem>
|
|
609
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
610
|
+
Import
|
|
611
|
+
</DropdownMenuItem>
|
|
612
|
+
|
|
613
|
+
<DropdownMenuSeparator />
|
|
614
|
+
|
|
615
|
+
<DropdownMenuItem onClick={() => setRefreshing(true)}>
|
|
616
|
+
<RefreshCw className="mr-2 h-4 w-4" />
|
|
617
|
+
Refresh Data
|
|
618
|
+
</DropdownMenuItem>
|
|
619
|
+
|
|
620
|
+
<DropdownMenuItem>
|
|
621
|
+
<Share2 className="mr-2 h-4 w-4" />
|
|
622
|
+
Share Dashboard
|
|
623
|
+
</DropdownMenuItem>
|
|
624
|
+
</DropdownMenuContent>
|
|
625
|
+
</DropdownMenu>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
{/* Edit mode banner */}
|
|
631
|
+
<AnimatePresence>
|
|
632
|
+
{editMode && (
|
|
633
|
+
<motion.div
|
|
634
|
+
initial={{ height: 0, opacity: 0 }}
|
|
635
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
636
|
+
exit={{ height: 0, opacity: 0 }}
|
|
637
|
+
className="bg-primary/10 border-t border-primary/20"
|
|
638
|
+
>
|
|
639
|
+
<div className="px-6 py-2 flex items-center justify-between">
|
|
640
|
+
<div className="flex items-center gap-2 text-sm">
|
|
641
|
+
<Sparkles className="h-4 w-4 text-primary" />
|
|
642
|
+
<span>Edit mode enabled - Drag widgets to rearrange</span>
|
|
643
|
+
</div>
|
|
644
|
+
<div className="flex items-center gap-2">
|
|
645
|
+
<Button
|
|
646
|
+
variant="ghost"
|
|
647
|
+
size="sm"
|
|
648
|
+
onClick={() => setEditMode(false)}
|
|
649
|
+
>
|
|
650
|
+
<X className="h-4 w-4 mr-1" />
|
|
651
|
+
Cancel
|
|
652
|
+
</Button>
|
|
653
|
+
<Button
|
|
654
|
+
variant="primary"
|
|
655
|
+
size="sm"
|
|
656
|
+
onClick={() => {
|
|
657
|
+
setEditMode(false)
|
|
658
|
+
// Save layout changes
|
|
659
|
+
}}
|
|
660
|
+
>
|
|
661
|
+
<Save className="h-4 w-4 mr-1" />
|
|
662
|
+
Save Changes
|
|
663
|
+
</Button>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
</motion.div>
|
|
667
|
+
)}
|
|
668
|
+
</AnimatePresence>
|
|
669
|
+
</motion.div>
|
|
313
670
|
)}
|
|
314
671
|
|
|
315
|
-
{/*
|
|
316
|
-
<div className="
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
672
|
+
{/* Main content */}
|
|
673
|
+
<div className="p-6">
|
|
674
|
+
{widgets.length === 0 ? (
|
|
675
|
+
// Empty state
|
|
676
|
+
<motion.div
|
|
677
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
678
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
679
|
+
className="flex flex-col items-center justify-center min-h-[400px] text-center"
|
|
680
|
+
>
|
|
681
|
+
<div className={cn(
|
|
682
|
+
"p-4 rounded-full mb-4",
|
|
683
|
+
glassmorphism ? "bg-background/60 backdrop-blur-sm" : "bg-muted"
|
|
684
|
+
)}>
|
|
685
|
+
<LayoutGrid className="h-8 w-8 text-muted-foreground" />
|
|
686
|
+
</div>
|
|
687
|
+
<h3 className="text-lg font-semibold mb-2">No widgets added yet</h3>
|
|
688
|
+
<p className="text-muted-foreground mb-4">
|
|
689
|
+
Start by adding widgets or choosing a template
|
|
690
|
+
</p>
|
|
691
|
+
<div className="flex gap-2">
|
|
692
|
+
<Button onClick={() => setShowWidgetLibrary(true)}>
|
|
693
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
694
|
+
Add Widget
|
|
695
|
+
</Button>
|
|
696
|
+
<Button variant="outline" onClick={() => setShowTemplates(true)}>
|
|
697
|
+
<LayoutGrid className="h-4 w-4 mr-2" />
|
|
698
|
+
Browse Templates
|
|
699
|
+
</Button>
|
|
700
|
+
</div>
|
|
701
|
+
</motion.div>
|
|
702
|
+
) : (
|
|
703
|
+
// Dashboard grid
|
|
704
|
+
<DashboardGrid
|
|
705
|
+
widgets={widgets}
|
|
706
|
+
onLayoutChange={(layout) => {
|
|
707
|
+
// Update widget positions
|
|
708
|
+
const updatedWidgets = widgets.map(widget => {
|
|
709
|
+
const layoutItem = layout.find(l => l.i === widget.id)
|
|
710
|
+
if (layoutItem) {
|
|
711
|
+
return {
|
|
712
|
+
...widget,
|
|
713
|
+
position: { x: layoutItem.x, y: layoutItem.y },
|
|
714
|
+
size: { ...widget.size, w: layoutItem.w, h: layoutItem.h }
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return widget
|
|
718
|
+
})
|
|
719
|
+
setWidgets(updatedWidgets)
|
|
720
|
+
}}
|
|
721
|
+
onWidgetRemove={(widgetId) => {
|
|
722
|
+
setWidgets(prev => prev.filter(w => w.id !== widgetId))
|
|
723
|
+
onWidgetRemove?.(widgetId)
|
|
724
|
+
}}
|
|
725
|
+
onWidgetAction={(widgetId, action, data) => {
|
|
726
|
+
console.log('Widget action:', widgetId, action, data)
|
|
727
|
+
// Handle widget actions
|
|
728
|
+
}}
|
|
729
|
+
editMode={editMode}
|
|
730
|
+
glassmorphism={glassmorphism}
|
|
731
|
+
/>
|
|
732
|
+
)}
|
|
329
733
|
</div>
|
|
330
|
-
|
|
734
|
+
|
|
735
|
+
{/* Modals */}
|
|
736
|
+
<WidgetLibrary />
|
|
737
|
+
<TemplateGallery />
|
|
738
|
+
</motion.div>
|
|
331
739
|
)
|
|
332
740
|
}
|
|
333
741
|
|