@moontra/moonui-pro 2.5.14 → 2.6.1
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 +96 -25
- package/dist/index.mjs +4270 -1237
- package/package.json +1 -1
- package/src/components/dashboard/demo.tsx +116 -2
- package/src/components/dashboard/index.tsx +318 -22
- package/src/components/dashboard/time-range-picker.tsx +317 -261
- package/src/components/sidebar/index.tsx +179 -2
- package/src/components/ui/calendar.tsx +376 -54
- package/src/components/ui/hover-card.tsx +29 -0
- package/src/components/ui/index.ts +4 -0
- package/src/styles/calendar.css +17 -81
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moontra/moonui-pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Premium React components for MoonUI - Advanced UI library with 50+ pro components including performance, interactive, and gesture components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import { Dashboard } from './index'
|
|
5
|
-
import { Widget, MetricData, ChartData, ActivityItem } from './types'
|
|
5
|
+
import { Widget, MetricData, ChartData, ActivityItem, DashboardNotification } from './types'
|
|
6
6
|
import { MetricCard } from './widgets/metric-card'
|
|
7
7
|
import { ChartWidget } from './widgets/chart-widget'
|
|
8
8
|
import { ActivityFeed } from './widgets/activity-feed'
|
|
@@ -14,7 +14,11 @@ import {
|
|
|
14
14
|
Package,
|
|
15
15
|
CreditCard,
|
|
16
16
|
Activity,
|
|
17
|
-
AlertCircle
|
|
17
|
+
AlertCircle,
|
|
18
|
+
User,
|
|
19
|
+
Settings2,
|
|
20
|
+
LogOut,
|
|
21
|
+
HelpCircle
|
|
18
22
|
} from 'lucide-react'
|
|
19
23
|
|
|
20
24
|
// Örnek metrik verileri
|
|
@@ -220,7 +224,107 @@ const sampleWidgets: Widget[] = [
|
|
|
220
224
|
}
|
|
221
225
|
]
|
|
222
226
|
|
|
227
|
+
// Örnek notification verileri
|
|
228
|
+
const sampleNotifications: DashboardNotification[] = [
|
|
229
|
+
{
|
|
230
|
+
id: 'notif-1',
|
|
231
|
+
type: 'success',
|
|
232
|
+
title: 'Payment Received',
|
|
233
|
+
message: 'Payment of $1,250 has been processed successfully',
|
|
234
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 5), // 5 dakika önce
|
|
235
|
+
read: false
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'notif-2',
|
|
239
|
+
type: 'warning',
|
|
240
|
+
title: 'Low Stock Alert',
|
|
241
|
+
message: 'Product "Premium Widget" is running low on stock',
|
|
242
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 30), // 30 dakika önce
|
|
243
|
+
read: false
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: 'notif-3',
|
|
247
|
+
type: 'info',
|
|
248
|
+
title: 'New Feature Available',
|
|
249
|
+
message: 'Check out our new analytics dashboard features',
|
|
250
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 saat önce
|
|
251
|
+
read: true
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: 'notif-4',
|
|
255
|
+
type: 'error',
|
|
256
|
+
title: 'Sync Failed',
|
|
257
|
+
message: 'Failed to sync data with external service',
|
|
258
|
+
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 gün önce
|
|
259
|
+
read: true
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
// Örnek user bilgileri
|
|
264
|
+
const sampleUser = {
|
|
265
|
+
name: 'John Doe',
|
|
266
|
+
email: 'john@example.com',
|
|
267
|
+
avatar: 'https://github.com/shadcn.png',
|
|
268
|
+
role: 'Admin'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Örnek custom user menu items
|
|
272
|
+
const customUserMenuItems = [
|
|
273
|
+
{
|
|
274
|
+
id: 'profile',
|
|
275
|
+
label: 'My Profile',
|
|
276
|
+
icon: <User className="h-4 w-4" />,
|
|
277
|
+
onClick: () => console.log('Profile clicked')
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
id: 'billing',
|
|
281
|
+
label: 'Billing & Plans',
|
|
282
|
+
icon: <CreditCard className="h-4 w-4" />,
|
|
283
|
+
onClick: () => console.log('Billing clicked')
|
|
284
|
+
},
|
|
285
|
+
{ id: 'sep1', separator: true },
|
|
286
|
+
{
|
|
287
|
+
id: 'settings',
|
|
288
|
+
label: 'Settings',
|
|
289
|
+
icon: <Settings2 className="h-4 w-4" />,
|
|
290
|
+
onClick: () => console.log('Settings clicked')
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
id: 'help',
|
|
294
|
+
label: 'Help & Support',
|
|
295
|
+
icon: <HelpCircle className="h-4 w-4" />,
|
|
296
|
+
onClick: () => console.log('Help clicked')
|
|
297
|
+
},
|
|
298
|
+
{ id: 'sep2', separator: true },
|
|
299
|
+
{
|
|
300
|
+
id: 'logout',
|
|
301
|
+
label: 'Sign Out',
|
|
302
|
+
icon: <LogOut className="h-4 w-4" />,
|
|
303
|
+
onClick: () => console.log('Logout clicked')
|
|
304
|
+
}
|
|
305
|
+
]
|
|
306
|
+
|
|
223
307
|
export function DashboardDemo() {
|
|
308
|
+
const [notifications, setNotifications] = React.useState(sampleNotifications)
|
|
309
|
+
|
|
310
|
+
const handleNotificationMarkAsRead = (notificationId: string) => {
|
|
311
|
+
setNotifications(prev =>
|
|
312
|
+
prev.map(n => n.id === notificationId ? { ...n, read: true } : n)
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const handleNotificationClear = (notificationId: string) => {
|
|
317
|
+
setNotifications(prev => prev.filter(n => n.id !== notificationId))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const handleNotificationMarkAllAsRead = () => {
|
|
321
|
+
setNotifications(prev => prev.map(n => ({ ...n, read: true })))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const handleNotificationClearAll = () => {
|
|
325
|
+
setNotifications([])
|
|
326
|
+
}
|
|
327
|
+
|
|
224
328
|
return (
|
|
225
329
|
<div className="min-h-screen bg-background">
|
|
226
330
|
<Dashboard
|
|
@@ -230,6 +334,16 @@ export function DashboardDemo() {
|
|
|
230
334
|
editable={true}
|
|
231
335
|
realtime={true}
|
|
232
336
|
glassmorphism={true}
|
|
337
|
+
user={sampleUser}
|
|
338
|
+
userMenuItems={customUserMenuItems}
|
|
339
|
+
notifications={notifications}
|
|
340
|
+
onNotificationClick={(notification) => {
|
|
341
|
+
console.log('Notification clicked:', notification)
|
|
342
|
+
}}
|
|
343
|
+
onNotificationMarkAsRead={handleNotificationMarkAsRead}
|
|
344
|
+
onNotificationMarkAllAsRead={handleNotificationMarkAllAsRead}
|
|
345
|
+
onNotificationClear={handleNotificationClear}
|
|
346
|
+
onNotificationClearAll={handleNotificationClearAll}
|
|
233
347
|
onWidgetAdd={(widget) => {
|
|
234
348
|
console.log('Widget added:', widget)
|
|
235
349
|
}}
|
|
@@ -7,6 +7,8 @@ import { Badge } from '../ui/badge'
|
|
|
7
7
|
import { Input } from '../ui/input'
|
|
8
8
|
import { ScrollArea } from '../ui/scroll-area'
|
|
9
9
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
|
|
10
|
+
import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'
|
|
11
|
+
import { Separator } from '../ui/separator'
|
|
10
12
|
import {
|
|
11
13
|
Activity,
|
|
12
14
|
Download,
|
|
@@ -36,7 +38,13 @@ import {
|
|
|
36
38
|
Clock,
|
|
37
39
|
Target,
|
|
38
40
|
ArrowUpRight,
|
|
39
|
-
CheckCircle
|
|
41
|
+
CheckCircle,
|
|
42
|
+
User,
|
|
43
|
+
LogOut,
|
|
44
|
+
Settings2,
|
|
45
|
+
AlertCircle,
|
|
46
|
+
Info,
|
|
47
|
+
CheckCheck
|
|
40
48
|
} from 'lucide-react'
|
|
41
49
|
import { cn } from '../../lib/utils'
|
|
42
50
|
import { DashboardGrid } from './dashboard-grid'
|
|
@@ -53,7 +61,8 @@ import {
|
|
|
53
61
|
ChartData,
|
|
54
62
|
ActivityItem,
|
|
55
63
|
DashboardTemplate,
|
|
56
|
-
WidgetType
|
|
64
|
+
WidgetType,
|
|
65
|
+
DashboardNotification
|
|
57
66
|
} from './types'
|
|
58
67
|
import {
|
|
59
68
|
DropdownMenu,
|
|
@@ -99,6 +108,49 @@ interface DashboardProps {
|
|
|
99
108
|
editable?: boolean
|
|
100
109
|
realtime?: boolean
|
|
101
110
|
glassmorphism?: boolean
|
|
111
|
+
|
|
112
|
+
// Notification yönetimi
|
|
113
|
+
notifications?: DashboardNotification[]
|
|
114
|
+
onNotificationClick?: (notification: DashboardNotification) => void
|
|
115
|
+
onNotificationMarkAsRead?: (notificationId: string) => void
|
|
116
|
+
onNotificationMarkAllAsRead?: () => void
|
|
117
|
+
onNotificationClear?: (notificationId: string) => void
|
|
118
|
+
onNotificationClearAll?: () => void
|
|
119
|
+
|
|
120
|
+
// User yönetimi
|
|
121
|
+
user?: {
|
|
122
|
+
name: string
|
|
123
|
+
email?: string
|
|
124
|
+
avatar?: string
|
|
125
|
+
role?: string
|
|
126
|
+
}
|
|
127
|
+
userMenuItems?: Array<{
|
|
128
|
+
id: string
|
|
129
|
+
label: string
|
|
130
|
+
icon?: React.ReactNode
|
|
131
|
+
onClick?: () => void
|
|
132
|
+
separator?: boolean
|
|
133
|
+
}>
|
|
134
|
+
onUserMenuClick?: () => void
|
|
135
|
+
onProfileClick?: () => void
|
|
136
|
+
onLogout?: () => void
|
|
137
|
+
|
|
138
|
+
// Header actions
|
|
139
|
+
onSearch?: (query: string) => void
|
|
140
|
+
onThemeChange?: (theme: DashboardTheme) => void
|
|
141
|
+
onMenuClick?: () => void
|
|
142
|
+
onRefresh?: () => void
|
|
143
|
+
|
|
144
|
+
// Custom header actions
|
|
145
|
+
headerActions?: React.ReactNode
|
|
146
|
+
|
|
147
|
+
// Time range yönetimi
|
|
148
|
+
timeRange?: TimeRange
|
|
149
|
+
onTimeRangeChange?: (range: TimeRange) => void
|
|
150
|
+
|
|
151
|
+
// Custom branding
|
|
152
|
+
logo?: React.ReactNode
|
|
153
|
+
brandName?: string
|
|
102
154
|
}
|
|
103
155
|
|
|
104
156
|
// Dashboard template'leri
|
|
@@ -230,6 +282,19 @@ const THEME_COLORS: Record<DashboardTheme, string> = {
|
|
|
230
282
|
custom: 'from-gray-500/10 to-gray-600/10'
|
|
231
283
|
}
|
|
232
284
|
|
|
285
|
+
// Relative time formatter
|
|
286
|
+
function formatRelativeTime(date: Date): string {
|
|
287
|
+
const now = new Date();
|
|
288
|
+
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
289
|
+
|
|
290
|
+
if (diffInSeconds < 60) return 'just now';
|
|
291
|
+
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
|
|
292
|
+
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
|
|
293
|
+
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
294
|
+
|
|
295
|
+
return date.toLocaleDateString();
|
|
296
|
+
}
|
|
297
|
+
|
|
233
298
|
export function Dashboard({
|
|
234
299
|
config,
|
|
235
300
|
widgets: initialWidgets = [],
|
|
@@ -246,24 +311,51 @@ export function Dashboard({
|
|
|
246
311
|
description = 'Real-time analytics and insights',
|
|
247
312
|
editable = true,
|
|
248
313
|
realtime = false,
|
|
249
|
-
glassmorphism = true
|
|
314
|
+
glassmorphism = true,
|
|
315
|
+
notifications = [],
|
|
316
|
+
onNotificationClick,
|
|
317
|
+
onNotificationMarkAsRead,
|
|
318
|
+
onNotificationMarkAllAsRead,
|
|
319
|
+
onNotificationClear,
|
|
320
|
+
onNotificationClearAll,
|
|
321
|
+
user,
|
|
322
|
+
userMenuItems,
|
|
323
|
+
onUserMenuClick,
|
|
324
|
+
onProfileClick,
|
|
325
|
+
onLogout,
|
|
326
|
+
onSearch,
|
|
327
|
+
onThemeChange,
|
|
328
|
+
onMenuClick,
|
|
329
|
+
onRefresh,
|
|
330
|
+
headerActions,
|
|
331
|
+
timeRange: propTimeRange,
|
|
332
|
+
onTimeRangeChange,
|
|
333
|
+
logo,
|
|
334
|
+
brandName
|
|
250
335
|
}: DashboardProps) {
|
|
251
336
|
// State yönetimi
|
|
252
337
|
const [editMode, setEditMode] = React.useState(false)
|
|
253
338
|
const [widgets, setWidgets] = React.useState<Widget[]>(initialWidgets)
|
|
254
339
|
const [selectedTheme, setSelectedTheme] = React.useState<DashboardTheme>('analytics')
|
|
255
|
-
const [timeRange, setTimeRange] = React.useState<TimeRange>()
|
|
340
|
+
const [timeRange, setTimeRange] = React.useState<TimeRange | undefined>(propTimeRange)
|
|
256
341
|
const [searchQuery, setSearchQuery] = React.useState('')
|
|
257
342
|
const [showWidgetLibrary, setShowWidgetLibrary] = React.useState(false)
|
|
258
343
|
const [showTemplates, setShowTemplates] = React.useState(false)
|
|
259
344
|
const [refreshing, setRefreshing] = React.useState(false)
|
|
260
|
-
const [
|
|
345
|
+
const [showNotifications, setShowNotifications] = React.useState(false)
|
|
261
346
|
|
|
262
347
|
// initialWidgets değiştiğinde state'i güncelle
|
|
263
348
|
React.useEffect(() => {
|
|
264
349
|
setWidgets(initialWidgets)
|
|
265
350
|
}, [initialWidgets])
|
|
266
351
|
|
|
352
|
+
// propTimeRange değiştiğinde state'i güncelle
|
|
353
|
+
React.useEffect(() => {
|
|
354
|
+
if (propTimeRange) {
|
|
355
|
+
setTimeRange(propTimeRange)
|
|
356
|
+
}
|
|
357
|
+
}, [propTimeRange])
|
|
358
|
+
|
|
267
359
|
// WebSocket bağlantısı (real-time için)
|
|
268
360
|
React.useEffect(() => {
|
|
269
361
|
if (!realtime) return
|
|
@@ -375,7 +467,10 @@ export function Dashboard({
|
|
|
375
467
|
<Input
|
|
376
468
|
placeholder="Search widgets..."
|
|
377
469
|
value={searchQuery}
|
|
378
|
-
onChange={(e) =>
|
|
470
|
+
onChange={(e) => {
|
|
471
|
+
setSearchQuery(e.target.value);
|
|
472
|
+
onSearch?.(e.target.value);
|
|
473
|
+
}}
|
|
379
474
|
className="pl-10"
|
|
380
475
|
/>
|
|
381
476
|
</div>
|
|
@@ -498,11 +593,26 @@ export function Dashboard({
|
|
|
498
593
|
whileHover={{ scale: 1.05 }}
|
|
499
594
|
whileTap={{ scale: 0.95 }}
|
|
500
595
|
>
|
|
501
|
-
<Button
|
|
596
|
+
<Button
|
|
597
|
+
variant="ghost"
|
|
598
|
+
size="sm"
|
|
599
|
+
className="lg:hidden"
|
|
600
|
+
onClick={onMenuClick}
|
|
601
|
+
>
|
|
502
602
|
<Menu className="h-5 w-5" />
|
|
503
603
|
</Button>
|
|
504
604
|
</motion.div>
|
|
505
605
|
|
|
606
|
+
{/* Logo and Brand */}
|
|
607
|
+
{(logo || brandName) && (
|
|
608
|
+
<div className="flex items-center gap-2">
|
|
609
|
+
{logo}
|
|
610
|
+
{brandName && (
|
|
611
|
+
<span className="font-semibold text-lg">{brandName}</span>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
|
|
506
616
|
<div>
|
|
507
617
|
<div className="flex items-center gap-2">
|
|
508
618
|
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
|
|
@@ -522,21 +632,127 @@ export function Dashboard({
|
|
|
522
632
|
{/* Time range picker */}
|
|
523
633
|
<TimeRangePicker
|
|
524
634
|
value={timeRange}
|
|
525
|
-
onChange={
|
|
635
|
+
onChange={(range) => {
|
|
636
|
+
setTimeRange(range);
|
|
637
|
+
onTimeRangeChange?.(range);
|
|
638
|
+
}}
|
|
526
639
|
glassmorphism={glassmorphism}
|
|
527
640
|
/>
|
|
528
641
|
|
|
529
642
|
{/* Notifications */}
|
|
530
|
-
<
|
|
531
|
-
<
|
|
532
|
-
<
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
643
|
+
<DropdownMenu open={showNotifications} onOpenChange={setShowNotifications}>
|
|
644
|
+
<DropdownMenuTrigger asChild>
|
|
645
|
+
<Button variant="ghost" size="sm" className="relative h-9 w-9 p-0">
|
|
646
|
+
<Bell className="h-5 w-5" />
|
|
647
|
+
{notifications.filter(n => !n.read).length > 0 && (
|
|
648
|
+
<span className="absolute -top-1 -right-1 min-w-[1rem] h-4 px-1 rounded-full bg-destructive text-[10px] font-medium text-destructive-foreground flex items-center justify-center">
|
|
649
|
+
{notifications.filter(n => !n.read).length}
|
|
650
|
+
</span>
|
|
651
|
+
)}
|
|
652
|
+
</Button>
|
|
653
|
+
</DropdownMenuTrigger>
|
|
654
|
+
<DropdownMenuContent
|
|
655
|
+
align="end"
|
|
656
|
+
className="w-80"
|
|
657
|
+
sideOffset={8}
|
|
658
|
+
>
|
|
659
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
660
|
+
<h4 className="text-sm font-semibold">Notifications</h4>
|
|
661
|
+
{notifications.length > 0 && (
|
|
662
|
+
<div className="flex items-center gap-2">
|
|
663
|
+
<Button
|
|
664
|
+
variant="ghost"
|
|
665
|
+
size="sm"
|
|
666
|
+
className="h-auto p-1 text-xs"
|
|
667
|
+
onClick={() => onNotificationMarkAllAsRead?.()}
|
|
668
|
+
>
|
|
669
|
+
<CheckCheck className="h-3 w-3 mr-1" />
|
|
670
|
+
Mark all read
|
|
671
|
+
</Button>
|
|
672
|
+
<Button
|
|
673
|
+
variant="ghost"
|
|
674
|
+
size="sm"
|
|
675
|
+
className="h-auto p-1 text-xs"
|
|
676
|
+
onClick={() => onNotificationClearAll?.()}
|
|
677
|
+
>
|
|
678
|
+
Clear all
|
|
679
|
+
</Button>
|
|
680
|
+
</div>
|
|
681
|
+
)}
|
|
682
|
+
</div>
|
|
683
|
+
<Separator />
|
|
684
|
+
<ScrollArea className="h-[400px]">
|
|
685
|
+
{notifications.length === 0 ? (
|
|
686
|
+
<div className="p-8 text-center text-sm text-muted-foreground">
|
|
687
|
+
<Bell className="h-8 w-8 mx-auto mb-2 opacity-20" />
|
|
688
|
+
<p>No notifications</p>
|
|
689
|
+
</div>
|
|
690
|
+
) : (
|
|
691
|
+
<div className="p-1">
|
|
692
|
+
{notifications.map((notification) => {
|
|
693
|
+
const Icon = notification.type === 'error' ? AlertCircle :
|
|
694
|
+
notification.type === 'warning' ? AlertCircle :
|
|
695
|
+
notification.type === 'success' ? CheckCircle :
|
|
696
|
+
Info;
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<motion.div
|
|
700
|
+
key={notification.id}
|
|
701
|
+
initial={{ opacity: 0, x: -20 }}
|
|
702
|
+
animate={{ opacity: 1, x: 0 }}
|
|
703
|
+
className={cn(
|
|
704
|
+
"flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors",
|
|
705
|
+
"hover:bg-muted/50",
|
|
706
|
+
!notification.read && "bg-muted/30"
|
|
707
|
+
)}
|
|
708
|
+
onClick={() => {
|
|
709
|
+
onNotificationClick?.(notification);
|
|
710
|
+
if (!notification.read) {
|
|
711
|
+
onNotificationMarkAsRead?.(notification.id);
|
|
712
|
+
}
|
|
713
|
+
}}
|
|
714
|
+
>
|
|
715
|
+
<div className={cn(
|
|
716
|
+
"mt-0.5 p-1.5 rounded-full",
|
|
717
|
+
notification.type === 'error' && "bg-destructive/10 text-destructive",
|
|
718
|
+
notification.type === 'warning' && "bg-yellow-500/10 text-yellow-600 dark:text-yellow-500",
|
|
719
|
+
notification.type === 'success' && "bg-green-500/10 text-green-600 dark:text-green-500",
|
|
720
|
+
notification.type === 'info' && "bg-blue-500/10 text-blue-600 dark:text-blue-500"
|
|
721
|
+
)}>
|
|
722
|
+
<Icon className="h-3.5 w-3.5" />
|
|
723
|
+
</div>
|
|
724
|
+
<div className="flex-1 space-y-1">
|
|
725
|
+
<p className="text-sm font-medium leading-none">
|
|
726
|
+
{notification.title}
|
|
727
|
+
</p>
|
|
728
|
+
{notification.message && (
|
|
729
|
+
<p className="text-xs text-muted-foreground">
|
|
730
|
+
{notification.message}
|
|
731
|
+
</p>
|
|
732
|
+
)}
|
|
733
|
+
<p className="text-xs text-muted-foreground">
|
|
734
|
+
{formatRelativeTime(notification.timestamp)}
|
|
735
|
+
</p>
|
|
736
|
+
</div>
|
|
737
|
+
<Button
|
|
738
|
+
variant="ghost"
|
|
739
|
+
size="sm"
|
|
740
|
+
className="h-6 w-6 p-0"
|
|
741
|
+
onClick={(e) => {
|
|
742
|
+
e.stopPropagation();
|
|
743
|
+
onNotificationClear?.(notification.id);
|
|
744
|
+
}}
|
|
745
|
+
>
|
|
746
|
+
<X className="h-3 w-3" />
|
|
747
|
+
</Button>
|
|
748
|
+
</motion.div>
|
|
749
|
+
);
|
|
750
|
+
})}
|
|
751
|
+
</div>
|
|
752
|
+
)}
|
|
753
|
+
</ScrollArea>
|
|
754
|
+
</DropdownMenuContent>
|
|
755
|
+
</DropdownMenu>
|
|
540
756
|
|
|
541
757
|
{/* Theme selector */}
|
|
542
758
|
<DropdownMenu>
|
|
@@ -546,19 +762,28 @@ export function Dashboard({
|
|
|
546
762
|
</Button>
|
|
547
763
|
</DropdownMenuTrigger>
|
|
548
764
|
<DropdownMenuContent align="end">
|
|
549
|
-
<DropdownMenuItem onClick={() =>
|
|
765
|
+
<DropdownMenuItem onClick={() => {
|
|
766
|
+
setSelectedTheme('analytics');
|
|
767
|
+
onThemeChange?.('analytics');
|
|
768
|
+
}}>
|
|
550
769
|
<div className="flex items-center gap-2">
|
|
551
770
|
<div className="h-4 w-4 rounded bg-gradient-to-br from-blue-500 to-purple-500" />
|
|
552
771
|
Analytics
|
|
553
772
|
</div>
|
|
554
773
|
</DropdownMenuItem>
|
|
555
|
-
<DropdownMenuItem onClick={() =>
|
|
774
|
+
<DropdownMenuItem onClick={() => {
|
|
775
|
+
setSelectedTheme('sales');
|
|
776
|
+
onThemeChange?.('sales');
|
|
777
|
+
}}>
|
|
556
778
|
<div className="flex items-center gap-2">
|
|
557
779
|
<div className="h-4 w-4 rounded bg-gradient-to-br from-green-500 to-emerald-500" />
|
|
558
780
|
Sales
|
|
559
781
|
</div>
|
|
560
782
|
</DropdownMenuItem>
|
|
561
|
-
<DropdownMenuItem onClick={() =>
|
|
783
|
+
<DropdownMenuItem onClick={() => {
|
|
784
|
+
setSelectedTheme('monitoring');
|
|
785
|
+
onThemeChange?.('monitoring');
|
|
786
|
+
}}>
|
|
562
787
|
<div className="flex items-center gap-2">
|
|
563
788
|
<div className="h-4 w-4 rounded bg-gradient-to-br from-orange-500 to-red-500" />
|
|
564
789
|
Monitoring
|
|
@@ -567,6 +792,73 @@ export function Dashboard({
|
|
|
567
792
|
</DropdownMenuContent>
|
|
568
793
|
</DropdownMenu>
|
|
569
794
|
|
|
795
|
+
{/* Custom header actions */}
|
|
796
|
+
{headerActions}
|
|
797
|
+
|
|
798
|
+
{/* User Profile */}
|
|
799
|
+
{user && (
|
|
800
|
+
<DropdownMenu>
|
|
801
|
+
<DropdownMenuTrigger asChild>
|
|
802
|
+
<Button variant="ghost" size="sm" className="relative h-8 w-8 rounded-full">
|
|
803
|
+
<Avatar className="h-8 w-8">
|
|
804
|
+
<AvatarImage src={user.avatar} alt={user.name} />
|
|
805
|
+
<AvatarFallback>
|
|
806
|
+
{user.name.split(' ').map(n => n[0]).join('').toUpperCase()}
|
|
807
|
+
</AvatarFallback>
|
|
808
|
+
</Avatar>
|
|
809
|
+
</Button>
|
|
810
|
+
</DropdownMenuTrigger>
|
|
811
|
+
<DropdownMenuContent align="end" className="w-56">
|
|
812
|
+
<div className="flex items-center justify-start gap-2 p-2">
|
|
813
|
+
<div className="flex flex-col space-y-1 leading-none">
|
|
814
|
+
{user.name && (
|
|
815
|
+
<p className="font-medium">{user.name}</p>
|
|
816
|
+
)}
|
|
817
|
+
{user.email && (
|
|
818
|
+
<p className="text-xs text-muted-foreground">
|
|
819
|
+
{user.email}
|
|
820
|
+
</p>
|
|
821
|
+
)}
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
<DropdownMenuSeparator />
|
|
825
|
+
{userMenuItems ? (
|
|
826
|
+
// Custom menu items
|
|
827
|
+
userMenuItems.map((item, index) => (
|
|
828
|
+
item.separator ? (
|
|
829
|
+
<DropdownMenuSeparator key={item.id || `sep-${index}`} />
|
|
830
|
+
) : (
|
|
831
|
+
<DropdownMenuItem
|
|
832
|
+
key={item.id}
|
|
833
|
+
onClick={() => item.onClick?.()}
|
|
834
|
+
>
|
|
835
|
+
{item.icon && <span className="mr-2">{item.icon}</span>}
|
|
836
|
+
{item.label}
|
|
837
|
+
</DropdownMenuItem>
|
|
838
|
+
)
|
|
839
|
+
))
|
|
840
|
+
) : (
|
|
841
|
+
// Default menu items
|
|
842
|
+
<>
|
|
843
|
+
<DropdownMenuItem onClick={() => onProfileClick?.()}>
|
|
844
|
+
<User className="mr-2 h-4 w-4" />
|
|
845
|
+
Profile
|
|
846
|
+
</DropdownMenuItem>
|
|
847
|
+
<DropdownMenuItem onClick={() => onUserMenuClick?.()}>
|
|
848
|
+
<Settings2 className="mr-2 h-4 w-4" />
|
|
849
|
+
Settings
|
|
850
|
+
</DropdownMenuItem>
|
|
851
|
+
<DropdownMenuSeparator />
|
|
852
|
+
<DropdownMenuItem onClick={() => onLogout?.()}>
|
|
853
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
854
|
+
Log out
|
|
855
|
+
</DropdownMenuItem>
|
|
856
|
+
</>
|
|
857
|
+
)}
|
|
858
|
+
</DropdownMenuContent>
|
|
859
|
+
</DropdownMenu>
|
|
860
|
+
)}
|
|
861
|
+
|
|
570
862
|
{/* More actions */}
|
|
571
863
|
<DropdownMenu>
|
|
572
864
|
<DropdownMenuTrigger asChild>
|
|
@@ -619,7 +911,11 @@ export function Dashboard({
|
|
|
619
911
|
|
|
620
912
|
<DropdownMenuSeparator />
|
|
621
913
|
|
|
622
|
-
<DropdownMenuItem onClick={() =>
|
|
914
|
+
<DropdownMenuItem onClick={() => {
|
|
915
|
+
setRefreshing(true);
|
|
916
|
+
onRefresh?.();
|
|
917
|
+
setTimeout(() => setRefreshing(false), 1000);
|
|
918
|
+
}}>
|
|
623
919
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
624
920
|
Refresh Data
|
|
625
921
|
</DropdownMenuItem>
|