@moontra/moonui-pro 2.20.1 → 2.20.2
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 +691 -261
- package/dist/index.mjs +7418 -4934
- package/package.json +4 -3
- package/scripts/postbuild.js +27 -0
- package/src/components/advanced-chart/index.tsx +5 -1
- package/src/components/advanced-forms/index.tsx +175 -16
- package/src/components/calendar/event-dialog.tsx +18 -13
- package/src/components/calendar/index.tsx +197 -50
- package/src/components/dashboard/dashboard-grid.tsx +21 -3
- package/src/components/dashboard/types.ts +3 -0
- package/src/components/dashboard/widgets/activity-feed.tsx +6 -1
- package/src/components/dashboard/widgets/comparison-widget.tsx +177 -0
- package/src/components/dashboard/widgets/index.ts +5 -0
- package/src/components/dashboard/widgets/metric-card.tsx +21 -1
- package/src/components/dashboard/widgets/progress-widget.tsx +113 -0
- package/src/components/error-boundary/index.tsx +160 -37
- package/src/components/form-wizard/form-wizard-context.tsx +54 -26
- package/src/components/form-wizard/form-wizard-progress.tsx +33 -2
- package/src/components/form-wizard/types.ts +2 -1
- package/src/components/github-stars/hooks.ts +1 -0
- package/src/components/github-stars/variants.tsx +3 -1
- package/src/components/health-check/index.tsx +14 -14
- package/src/components/hover-card-3d/index.tsx +2 -3
- package/src/components/index.ts +5 -3
- package/src/components/kanban/kanban.tsx +23 -18
- package/src/components/license-error/index.tsx +2 -0
- package/src/components/magnetic-button/index.tsx +56 -7
- package/src/components/memory-efficient-data/index.tsx +117 -115
- package/src/components/navbar/index.tsx +781 -0
- package/src/components/performance-debugger/index.tsx +62 -38
- package/src/components/performance-monitor/index.tsx +47 -33
- package/src/components/phone-number-input/index.tsx +32 -27
- package/src/components/phone-number-input/phone-number-input-simple.tsx +167 -0
- package/src/components/rich-text-editor/index.tsx +26 -28
- package/src/components/rich-text-editor/slash-commands-extension.ts +15 -5
- package/src/components/sidebar/index.tsx +32 -13
- package/src/components/timeline/index.tsx +84 -49
- package/src/components/ui/accordion.tsx +550 -42
- package/src/components/ui/avatar.tsx +2 -0
- package/src/components/ui/badge.tsx +2 -0
- package/src/components/ui/breadcrumb.tsx +2 -0
- package/src/components/ui/button.tsx +39 -33
- package/src/components/ui/card.tsx +2 -0
- package/src/components/ui/collapsible.tsx +546 -50
- package/src/components/ui/command.tsx +790 -67
- package/src/components/ui/dialog.tsx +510 -92
- package/src/components/ui/dropdown-menu.tsx +540 -52
- package/src/components/ui/index.ts +37 -5
- package/src/components/ui/input.tsx +2 -0
- package/src/components/ui/magnetic-button.tsx +1 -1
- package/src/components/ui/media-gallery.tsx +1 -2
- package/src/components/ui/navigation-menu.tsx +130 -0
- package/src/components/ui/pagination.tsx +2 -0
- package/src/components/ui/select.tsx +6 -2
- package/src/components/ui/spotlight-card.tsx +1 -1
- package/src/components/ui/table.tsx +2 -0
- package/src/components/ui/tabs-pro.tsx +542 -0
- package/src/components/ui/tabs.tsx +23 -167
- package/src/components/ui/toggle.tsx +12 -12
- package/src/index.ts +11 -3
- package/src/styles/index.css +596 -0
- package/src/use-performance-optimizer.ts +1 -1
- package/src/utils/chart-helpers.ts +1 -1
- package/src/__tests__/use-intersection-observer.test.tsx +0 -216
- package/src/__tests__/use-local-storage.test.tsx +0 -174
- package/src/__tests__/use-pro-access.test.tsx +0 -183
- package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
- package/src/components/data-table/data-table.test.tsx +0 -187
- package/src/components/enhanced/badge.tsx +0 -191
- package/src/components/enhanced/button.tsx +0 -362
- package/src/components/enhanced/card.tsx +0 -266
- package/src/components/enhanced/dialog.tsx +0 -246
- package/src/components/enhanced/index.ts +0 -4
- package/src/components/file-upload/file-upload.test.tsx +0 -243
- package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
- package/src/types/moonui.d.ts +0 -22
|
@@ -38,7 +38,9 @@ import {
|
|
|
38
38
|
Zap,
|
|
39
39
|
Sun,
|
|
40
40
|
Moon,
|
|
41
|
-
Palette
|
|
41
|
+
Palette,
|
|
42
|
+
Menu,
|
|
43
|
+
X
|
|
42
44
|
} from 'lucide-react'
|
|
43
45
|
import { cn } from '../../lib/utils'
|
|
44
46
|
import { EventDialog } from './event-dialog'
|
|
@@ -228,6 +230,8 @@ export function Calendar({
|
|
|
228
230
|
const [showFiltersPanel, setShowFiltersPanel] = React.useState(false)
|
|
229
231
|
const [selectedTags, setSelectedTags] = React.useState<string[]>([])
|
|
230
232
|
const [miniCalendarDate, setMiniCalendarDate] = React.useState(new Date())
|
|
233
|
+
const [isSidebarOpen, setIsSidebarOpen] = React.useState(false) // Mobile sidebar state
|
|
234
|
+
const [isDesktopSidebarCollapsed, setIsDesktopSidebarCollapsed] = React.useState(false) // Desktop sidebar state
|
|
231
235
|
|
|
232
236
|
const today = new Date()
|
|
233
237
|
const currentMonth = currentDate.getMonth()
|
|
@@ -550,42 +554,184 @@ export function Calendar({
|
|
|
550
554
|
|
|
551
555
|
return (
|
|
552
556
|
<>
|
|
553
|
-
<
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
<div
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
557
|
+
<div className={cn("w-full flex relative", className)} style={{ height: height ? `${height}px` : undefined }}>
|
|
558
|
+
{/* Mobile Sidebar Overlay */}
|
|
559
|
+
{isSidebarOpen && (
|
|
560
|
+
<div
|
|
561
|
+
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
|
|
562
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
563
|
+
/>
|
|
564
|
+
)}
|
|
565
|
+
|
|
566
|
+
{/* Sidebar */}
|
|
567
|
+
<aside className={cn(
|
|
568
|
+
"bg-card border-r transition-all duration-300 flex-shrink-0 overflow-hidden",
|
|
569
|
+
// Mobile styles
|
|
570
|
+
"fixed inset-y-0 left-0 z-50 lg:relative lg:inset-auto",
|
|
571
|
+
isSidebarOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0",
|
|
572
|
+
// Desktop styles
|
|
573
|
+
isDesktopSidebarCollapsed ? "lg:w-0" : "lg:w-64"
|
|
574
|
+
)}>
|
|
575
|
+
<div className="w-64 h-full flex flex-col">
|
|
576
|
+
{/* Sidebar Header */}
|
|
577
|
+
<div className="p-4 border-b">
|
|
578
|
+
<div className="flex items-center justify-between">
|
|
579
|
+
<h3 className="font-semibold text-sm">Calendar</h3>
|
|
580
|
+
<Button
|
|
581
|
+
variant="ghost"
|
|
582
|
+
size="sm"
|
|
583
|
+
className="lg:hidden"
|
|
584
|
+
onClick={() => setIsSidebarOpen(false)}
|
|
585
|
+
>
|
|
586
|
+
<X className="h-4 w-4" />
|
|
587
|
+
</Button>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
{/* Mini Calendar */}
|
|
592
|
+
<div className="p-4 border-b">
|
|
593
|
+
<div className="text-xs font-medium mb-2">
|
|
594
|
+
{MONTHS[miniCalendarDate.getMonth()]} {miniCalendarDate.getFullYear()}
|
|
595
|
+
</div>
|
|
596
|
+
<div className="grid grid-cols-7 gap-1 text-xs">
|
|
597
|
+
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((day, index) => (
|
|
598
|
+
<div key={`mini-day-${index}`} className="text-center text-muted-foreground">
|
|
599
|
+
{day}
|
|
600
|
+
</div>
|
|
601
|
+
))}
|
|
602
|
+
{Array.from({ length: 35 }, (_, i) => {
|
|
603
|
+
const date = new Date(miniCalendarDate.getFullYear(), miniCalendarDate.getMonth(), i - new Date(miniCalendarDate.getFullYear(), miniCalendarDate.getMonth(), 1).getDay() + 1)
|
|
604
|
+
const isCurrentMonth = date.getMonth() === miniCalendarDate.getMonth()
|
|
605
|
+
const isToday = date.toDateString() === today.toDateString()
|
|
606
|
+
const hasEvents = getEventsForDate(date).length > 0
|
|
607
|
+
|
|
608
|
+
return (
|
|
609
|
+
<button
|
|
610
|
+
key={i}
|
|
611
|
+
className={cn(
|
|
612
|
+
"p-1 rounded hover:bg-muted/50 transition-colors",
|
|
613
|
+
!isCurrentMonth && "text-muted-foreground/50",
|
|
614
|
+
isToday && "bg-primary text-primary-foreground",
|
|
615
|
+
hasEvents && !isToday && "font-bold"
|
|
616
|
+
)}
|
|
617
|
+
onClick={() => {
|
|
618
|
+
setCurrentDate(date)
|
|
619
|
+
setSelectedDate(date)
|
|
620
|
+
setIsSidebarOpen(false)
|
|
621
|
+
}}
|
|
622
|
+
>
|
|
623
|
+
{date.getDate()}
|
|
624
|
+
</button>
|
|
625
|
+
)
|
|
626
|
+
})}
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
{/* Categories Filter */}
|
|
631
|
+
<div className="p-4 space-y-2 flex-1 overflow-y-auto">
|
|
632
|
+
<h4 className="text-xs font-medium mb-2">Categories</h4>
|
|
633
|
+
<button
|
|
634
|
+
onClick={() => setFilterType('all')}
|
|
635
|
+
className={cn(
|
|
636
|
+
"w-full text-left px-2 py-1 rounded text-xs hover:bg-muted/50 transition-colors",
|
|
637
|
+
filterType === 'all' && "bg-muted"
|
|
638
|
+
)}
|
|
639
|
+
>
|
|
640
|
+
All Events
|
|
641
|
+
</button>
|
|
642
|
+
{['Personal', 'Work', 'Meeting', 'Task', 'Reminder', 'Holiday', 'Birthday', 'Other'].map((type) => (
|
|
643
|
+
<button
|
|
644
|
+
key={type}
|
|
645
|
+
onClick={() => setFilterType(type.toLowerCase())}
|
|
646
|
+
className={cn(
|
|
647
|
+
"w-full text-left px-2 py-1 rounded text-xs hover:bg-muted/50 transition-colors flex items-center gap-2",
|
|
648
|
+
filterType === type.toLowerCase() && "bg-muted"
|
|
649
|
+
)}
|
|
650
|
+
>
|
|
651
|
+
<div className={cn("w-2 h-2 rounded-full", EVENT_COLORS[type.toLowerCase() as keyof typeof EVENT_COLORS] || "bg-gray-500")} />
|
|
652
|
+
{type}
|
|
653
|
+
</button>
|
|
654
|
+
))}
|
|
655
|
+
|
|
656
|
+
<div className="pt-4">
|
|
657
|
+
<Button
|
|
658
|
+
className="w-full"
|
|
659
|
+
size="sm"
|
|
660
|
+
onClick={() => {
|
|
661
|
+
setEventDialogMode('create')
|
|
662
|
+
setSelectedEvent(null)
|
|
663
|
+
setEventDialogOpen(true)
|
|
664
|
+
setIsSidebarOpen(false)
|
|
665
|
+
}}
|
|
666
|
+
disabled={disabled}
|
|
667
|
+
>
|
|
668
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
669
|
+
New Event
|
|
670
|
+
</Button>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
585
673
|
</div>
|
|
586
|
-
</
|
|
587
|
-
|
|
588
|
-
|
|
674
|
+
</aside>
|
|
675
|
+
|
|
676
|
+
{/* Main Content */}
|
|
677
|
+
<Card className="flex-1 border-0 rounded-none">
|
|
678
|
+
<CardHeader>
|
|
679
|
+
<div className="flex items-center justify-between">
|
|
680
|
+
<div className="flex items-center gap-2">
|
|
681
|
+
{/* Mobile Menu Button */}
|
|
682
|
+
<Button
|
|
683
|
+
variant="ghost"
|
|
684
|
+
size="sm"
|
|
685
|
+
className="lg:hidden"
|
|
686
|
+
onClick={() => setIsSidebarOpen(true)}
|
|
687
|
+
>
|
|
688
|
+
<Menu className="h-4 w-4" />
|
|
689
|
+
</Button>
|
|
690
|
+
|
|
691
|
+
{/* Desktop Sidebar Toggle */}
|
|
692
|
+
<Button
|
|
693
|
+
variant="ghost"
|
|
694
|
+
size="sm"
|
|
695
|
+
className="hidden lg:block"
|
|
696
|
+
onClick={() => setIsDesktopSidebarCollapsed(!isDesktopSidebarCollapsed)}
|
|
697
|
+
>
|
|
698
|
+
<Menu className="h-4 w-4" />
|
|
699
|
+
</Button>
|
|
700
|
+
|
|
701
|
+
<div>
|
|
702
|
+
<CardTitle className="flex items-center gap-2">
|
|
703
|
+
<CalendarIcon className="h-5 w-5" />
|
|
704
|
+
{view === 'month' && `${MONTHS[currentMonth]} ${currentYear}`}
|
|
705
|
+
{view === 'week' && `Week of ${currentDate.toLocaleDateString()}`}
|
|
706
|
+
{view === 'day' && currentDate.toLocaleDateString()}
|
|
707
|
+
{view === 'agenda' && 'Agenda'}
|
|
708
|
+
</CardTitle>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
<div className="flex items-center gap-2">
|
|
712
|
+
<Button variant="outline" size="sm" onClick={goToToday} className="hidden sm:flex">
|
|
713
|
+
Today
|
|
714
|
+
</Button>
|
|
715
|
+
<Button
|
|
716
|
+
variant="outline"
|
|
717
|
+
size="sm"
|
|
718
|
+
onClick={() => navigateMonth('prev')}
|
|
719
|
+
disabled={disabled}
|
|
720
|
+
>
|
|
721
|
+
<ChevronLeft className="h-4 w-4" />
|
|
722
|
+
</Button>
|
|
723
|
+
<Button
|
|
724
|
+
variant="outline"
|
|
725
|
+
size="sm"
|
|
726
|
+
onClick={() => navigateMonth('next')}
|
|
727
|
+
disabled={disabled}
|
|
728
|
+
>
|
|
729
|
+
<ChevronRight className="h-4 w-4" />
|
|
730
|
+
</Button>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
</CardHeader>
|
|
734
|
+
<CardContent>
|
|
589
735
|
<div className="space-y-4">
|
|
590
736
|
{/* Calendar View */}
|
|
591
737
|
{view === 'month' && (
|
|
@@ -1052,21 +1198,22 @@ export function Calendar({
|
|
|
1052
1198
|
</div>
|
|
1053
1199
|
</div>
|
|
1054
1200
|
)}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1201
|
+
</div>
|
|
1202
|
+
</CardContent>
|
|
1203
|
+
</Card>
|
|
1204
|
+
</div>
|
|
1058
1205
|
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1206
|
+
{/* Event Dialog */}
|
|
1207
|
+
<EventDialog
|
|
1208
|
+
open={eventDialogOpen}
|
|
1209
|
+
onOpenChange={setEventDialogOpen}
|
|
1210
|
+
event={selectedEvent}
|
|
1211
|
+
selectedDate={selectedDate}
|
|
1212
|
+
onSave={handleEventSave}
|
|
1213
|
+
onDelete={handleEventDialogDelete}
|
|
1214
|
+
mode={eventDialogMode}
|
|
1215
|
+
/>
|
|
1216
|
+
</>
|
|
1070
1217
|
)
|
|
1071
1218
|
}
|
|
1072
1219
|
|
|
@@ -7,6 +7,8 @@ import { Widget } from './types'
|
|
|
7
7
|
import { MetricCard } from './widgets/metric-card'
|
|
8
8
|
import { ChartWidget } from './widgets/chart-widget'
|
|
9
9
|
import { ActivityFeed } from './widgets/activity-feed'
|
|
10
|
+
import { ProgressWidget } from './widgets/progress-widget'
|
|
11
|
+
import { ComparisonWidget } from './widgets/comparison-widget'
|
|
10
12
|
import { Responsive, WidthProvider, Layout, Layouts } from 'react-grid-layout'
|
|
11
13
|
const ResponsiveGridLayout = WidthProvider(Responsive)
|
|
12
14
|
import 'react-grid-layout/css/styles.css'
|
|
@@ -219,11 +221,27 @@ export function DashboardGrid({
|
|
|
219
221
|
/>
|
|
220
222
|
)
|
|
221
223
|
|
|
224
|
+
case 'progress':
|
|
225
|
+
return (
|
|
226
|
+
<ProgressWidget
|
|
227
|
+
data={widget.data}
|
|
228
|
+
title={widget.title}
|
|
229
|
+
glassmorphism={glassmorphism}
|
|
230
|
+
/>
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
case 'comparison':
|
|
234
|
+
return (
|
|
235
|
+
<ComparisonWidget
|
|
236
|
+
data={widget.data}
|
|
237
|
+
title={widget.title}
|
|
238
|
+
glassmorphism={glassmorphism}
|
|
239
|
+
/>
|
|
240
|
+
)
|
|
241
|
+
|
|
222
242
|
case 'table':
|
|
223
243
|
case 'map':
|
|
224
244
|
case 'calendar':
|
|
225
|
-
case 'progress':
|
|
226
|
-
case 'comparison':
|
|
227
245
|
// Bu widget tipleri için geçici olarak basit bir görünüm
|
|
228
246
|
return (
|
|
229
247
|
<div className={cn(
|
|
@@ -260,7 +278,7 @@ export function DashboardGrid({
|
|
|
260
278
|
transition={{ duration: 0.2 }}
|
|
261
279
|
>
|
|
262
280
|
{/* Widget içeriği */}
|
|
263
|
-
<div className="h-full">
|
|
281
|
+
<div className="h-full dashboard-widget-container">
|
|
264
282
|
{widgetContent()}
|
|
265
283
|
</div>
|
|
266
284
|
|
|
@@ -61,6 +61,11 @@ export function ActivityFeed({
|
|
|
61
61
|
const [filter, setFilter] = React.useState<'all' | 'info' | 'success' | 'warning' | 'error'>('all')
|
|
62
62
|
const [notificationsEnabled, setNotificationsEnabled] = React.useState(true)
|
|
63
63
|
const [newItems, setNewItems] = React.useState<ActivityItem[]>([])
|
|
64
|
+
const [isMounted, setIsMounted] = React.useState(false)
|
|
65
|
+
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
setIsMounted(true)
|
|
68
|
+
}, [])
|
|
64
69
|
|
|
65
70
|
// Simüle edilmiş real-time güncellemeler
|
|
66
71
|
React.useEffect(() => {
|
|
@@ -179,7 +184,7 @@ export function ActivityFeed({
|
|
|
179
184
|
{/* Zaman damgası */}
|
|
180
185
|
<div className="flex items-center gap-1 text-xs text-muted-foreground whitespace-nowrap">
|
|
181
186
|
<Clock className="h-3 w-3" />
|
|
182
|
-
{formatDistanceToNow(item.timestamp, { addSuffix: true })}
|
|
187
|
+
{isMounted ? formatDistanceToNow(item.timestamp, { addSuffix: true }) : 'Loading...'}
|
|
183
188
|
</div>
|
|
184
189
|
</div>
|
|
185
190
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { motion } from 'framer-motion'
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card'
|
|
6
|
+
import { cn } from '../../../lib/utils'
|
|
7
|
+
import { ComparisonData } from '../types'
|
|
8
|
+
import { ArrowUpRight, ArrowDownRight, Minus, BarChart3 } from 'lucide-react'
|
|
9
|
+
import { Badge } from '../../ui/badge'
|
|
10
|
+
|
|
11
|
+
interface ComparisonWidgetProps {
|
|
12
|
+
data: ComparisonData
|
|
13
|
+
title?: string
|
|
14
|
+
className?: string
|
|
15
|
+
glassmorphism?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ComparisonWidget({
|
|
19
|
+
data,
|
|
20
|
+
title = "Comparison",
|
|
21
|
+
className,
|
|
22
|
+
glassmorphism = false
|
|
23
|
+
}: ComparisonWidgetProps) {
|
|
24
|
+
const [isMounted, setIsMounted] = React.useState(false)
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
setIsMounted(true)
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
// En yüksek değeri bul
|
|
31
|
+
const maxValue = Math.max(...data.periods.map(p => p.value))
|
|
32
|
+
|
|
33
|
+
// İlk iki period arasındaki değişimi hesapla
|
|
34
|
+
const change = data.periods.length >= 2
|
|
35
|
+
? ((data.periods[0].value - data.periods[1].value) / data.periods[1].value) * 100
|
|
36
|
+
: 0
|
|
37
|
+
|
|
38
|
+
const changeType = change > 0 ? 'increase' : change < 0 ? 'decrease' : 'neutral'
|
|
39
|
+
|
|
40
|
+
// Değer formatla
|
|
41
|
+
const formatValue = (value: number) => {
|
|
42
|
+
if (!isMounted) return value.toString()
|
|
43
|
+
|
|
44
|
+
if (value >= 1000000) {
|
|
45
|
+
return (value / 1000000).toFixed(1) + 'M'
|
|
46
|
+
} else if (value >= 1000) {
|
|
47
|
+
return (value / 1000).toFixed(0) + 'K'
|
|
48
|
+
}
|
|
49
|
+
return value.toLocaleString()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Card className={cn(
|
|
54
|
+
"h-full flex flex-col overflow-hidden",
|
|
55
|
+
glassmorphism && "bg-background/60 backdrop-blur-md border-white/10",
|
|
56
|
+
className
|
|
57
|
+
)}>
|
|
58
|
+
<CardHeader className="flex-shrink-0 pb-3 px-4">
|
|
59
|
+
<div className="flex items-start justify-between gap-2">
|
|
60
|
+
<div className="flex-1 min-w-0">
|
|
61
|
+
<CardTitle className="text-sm font-semibold flex items-center gap-2">
|
|
62
|
+
<BarChart3 className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
63
|
+
<span className="truncate">{title}</span>
|
|
64
|
+
</CardTitle>
|
|
65
|
+
{data.metric && (
|
|
66
|
+
<p className="text-xs text-muted-foreground mt-1 truncate">{data.metric}</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
{change !== 0 && (
|
|
70
|
+
<div
|
|
71
|
+
className={cn(
|
|
72
|
+
"inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium flex-shrink-0",
|
|
73
|
+
changeType === 'increase' && "bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20",
|
|
74
|
+
changeType === 'decrease' && "bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/20",
|
|
75
|
+
changeType === 'neutral' && "bg-muted text-muted-foreground border border-border"
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{changeType === 'increase' ? <ArrowUpRight className="h-3 w-3" /> :
|
|
79
|
+
changeType === 'decrease' ? <ArrowDownRight className="h-3 w-3" /> :
|
|
80
|
+
<Minus className="h-3 w-3" />}
|
|
81
|
+
<span>
|
|
82
|
+
{Math.abs(change).toFixed(1)}%
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
</CardHeader>
|
|
88
|
+
|
|
89
|
+
<CardContent className="flex-1 px-4 pb-4 overflow-hidden flex flex-col gap-3">
|
|
90
|
+
{/* Bar list görünümü - scrollable area */}
|
|
91
|
+
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden pr-2 -mr-2">
|
|
92
|
+
<div className="space-y-2.5">
|
|
93
|
+
{data.periods.map((period, index) => {
|
|
94
|
+
const percentage = (period.value / maxValue) * 100
|
|
95
|
+
const isHighest = period.value === maxValue
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<motion.div
|
|
99
|
+
key={period.label}
|
|
100
|
+
initial={{ opacity: 0, x: -20 }}
|
|
101
|
+
animate={{ opacity: 1, x: 0 }}
|
|
102
|
+
transition={{ delay: index * 0.05 }}
|
|
103
|
+
>
|
|
104
|
+
{/* Label ve değer */}
|
|
105
|
+
<div className="flex items-center justify-between gap-3 mb-1">
|
|
106
|
+
<span className={cn(
|
|
107
|
+
"text-xs font-medium truncate flex-1",
|
|
108
|
+
isHighest ? "text-foreground" : "text-muted-foreground"
|
|
109
|
+
)}>
|
|
110
|
+
{period.label}
|
|
111
|
+
</span>
|
|
112
|
+
<span className={cn(
|
|
113
|
+
"text-xs font-semibold tabular-nums flex-shrink-0",
|
|
114
|
+
isHighest && "text-primary"
|
|
115
|
+
)}>
|
|
116
|
+
{formatValue(period.value)}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Progress bar */}
|
|
121
|
+
<div className="w-full bg-muted rounded-full h-1.5 overflow-hidden">
|
|
122
|
+
<motion.div
|
|
123
|
+
className={cn(
|
|
124
|
+
"h-full rounded-full transition-colors",
|
|
125
|
+
isHighest ? "bg-primary" : "bg-primary/60"
|
|
126
|
+
)}
|
|
127
|
+
initial={{ width: 0 }}
|
|
128
|
+
animate={{ width: `${Math.min(percentage, 100)}%` }}
|
|
129
|
+
transition={{ duration: 0.5, delay: index * 0.05 }}
|
|
130
|
+
/>
|
|
131
|
+
</div>
|
|
132
|
+
</motion.div>
|
|
133
|
+
)
|
|
134
|
+
})}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Compact chart view - fixed height */}
|
|
139
|
+
{data.showChart && (
|
|
140
|
+
<div className="flex-shrink-0 pt-3 border-t">
|
|
141
|
+
<div className="h-14 flex items-end justify-between gap-1">
|
|
142
|
+
{data.periods.slice(0, 5).map((period, index) => {
|
|
143
|
+
const height = (period.value / maxValue) * 100
|
|
144
|
+
const isHighest = period.value === maxValue
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<motion.div
|
|
148
|
+
key={period.label}
|
|
149
|
+
className="flex-1 flex flex-col items-center gap-0.5"
|
|
150
|
+
initial={{ opacity: 0 }}
|
|
151
|
+
animate={{ opacity: 1 }}
|
|
152
|
+
transition={{ delay: index * 0.05 }}
|
|
153
|
+
>
|
|
154
|
+
<div className="w-full h-10 flex items-end justify-center px-0.5">
|
|
155
|
+
<motion.div
|
|
156
|
+
className={cn(
|
|
157
|
+
"w-full max-w-[20px] rounded-t transition-colors",
|
|
158
|
+
isHighest ? "bg-primary" : "bg-primary/40"
|
|
159
|
+
)}
|
|
160
|
+
initial={{ height: 0 }}
|
|
161
|
+
animate={{ height: `${height}%` }}
|
|
162
|
+
transition={{ duration: 0.5, delay: index * 0.05 }}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
<span className="text-[9px] text-muted-foreground">
|
|
166
|
+
{period.label.slice(0, 3)}
|
|
167
|
+
</span>
|
|
168
|
+
</motion.div>
|
|
169
|
+
)
|
|
170
|
+
})}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</CardContent>
|
|
175
|
+
</Card>
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -47,6 +47,11 @@ export function MetricCard({
|
|
|
47
47
|
glassmorphism = false
|
|
48
48
|
}: MetricCardProps) {
|
|
49
49
|
const [isHovered, setIsHovered] = React.useState(false)
|
|
50
|
+
const [isMounted, setIsMounted] = React.useState(false)
|
|
51
|
+
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
setIsMounted(true)
|
|
54
|
+
}, [])
|
|
50
55
|
|
|
51
56
|
// Renk sınıfları
|
|
52
57
|
const colorClasses = {
|
|
@@ -173,13 +178,28 @@ export function MetricCard({
|
|
|
173
178
|
|
|
174
179
|
// Değer formatı
|
|
175
180
|
const formatValue = (value: string | number): string => {
|
|
181
|
+
if (!isMounted) {
|
|
182
|
+
// Server-side: basit format kullan
|
|
183
|
+
if (typeof value === 'number') {
|
|
184
|
+
if (value >= 1000000) {
|
|
185
|
+
return (value / 1000000).toFixed(1) + 'M'
|
|
186
|
+
} else if (value >= 1000) {
|
|
187
|
+
return (value / 1000).toFixed(1) + 'K'
|
|
188
|
+
}
|
|
189
|
+
return value.toString()
|
|
190
|
+
}
|
|
191
|
+
return value.toString()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Client-side: locale formatting kullanabilir
|
|
176
195
|
if (typeof value === 'number') {
|
|
177
196
|
if (value >= 1000000) {
|
|
178
197
|
return (value / 1000000).toFixed(1) + 'M'
|
|
179
198
|
} else if (value >= 1000) {
|
|
180
199
|
return (value / 1000).toFixed(1) + 'K'
|
|
181
200
|
}
|
|
182
|
-
|
|
201
|
+
// Nokta kullan, virgül değil
|
|
202
|
+
return value.toFixed(2)
|
|
183
203
|
}
|
|
184
204
|
return value.toString()
|
|
185
205
|
}
|