@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.
@@ -0,0 +1,418 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { motion, AnimatePresence } from 'framer-motion'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card'
6
+ import { Button } from '../../ui/button'
7
+ import { cn } from '../../../lib/utils'
8
+ import {
9
+ LineChart,
10
+ Line,
11
+ BarChart,
12
+ Bar,
13
+ AreaChart,
14
+ Area,
15
+ PieChart,
16
+ Pie,
17
+ RadarChart,
18
+ PolarGrid,
19
+ PolarAngleAxis,
20
+ PolarRadiusAxis,
21
+ Radar,
22
+ XAxis,
23
+ YAxis,
24
+ CartesianGrid,
25
+ Tooltip,
26
+ Legend,
27
+ ResponsiveContainer,
28
+ Cell
29
+ } from 'recharts'
30
+ import {
31
+ MoreVertical,
32
+ Download,
33
+ Maximize2,
34
+ RefreshCw,
35
+ Palette,
36
+ Settings,
37
+ Filter
38
+ } from 'lucide-react'
39
+ import {
40
+ DropdownMenu,
41
+ DropdownMenuContent,
42
+ DropdownMenuItem,
43
+ DropdownMenuSeparator,
44
+ DropdownMenuTrigger,
45
+ DropdownMenuSub,
46
+ DropdownMenuSubContent,
47
+ DropdownMenuSubTrigger,
48
+ } from '../../ui/dropdown-menu'
49
+ import { ChartData } from '../types'
50
+
51
+ interface ChartWidgetProps {
52
+ id: string
53
+ title: string
54
+ description?: string
55
+ data: ChartData
56
+ className?: string
57
+ height?: number
58
+ onAction?: (action: string, data?: any) => void
59
+ loading?: boolean
60
+ refreshing?: boolean
61
+ glassmorphism?: boolean
62
+ interactive?: boolean
63
+ }
64
+
65
+ // Renk paleti
66
+ const CHART_COLORS = {
67
+ default: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'],
68
+ pastel: ['#93c5fd', '#86efac', '#fcd34d', '#fca5a5', '#c4b5fd', '#fbcfe8'],
69
+ vibrant: ['#2563eb', '#059669', '#d97706', '#dc2626', '#7c3aed', '#db2777'],
70
+ monochrome: ['#1f2937', '#374151', '#4b5563', '#6b7280', '#9ca3af', '#d1d5db']
71
+ }
72
+
73
+ export function ChartWidget({
74
+ id,
75
+ title,
76
+ description,
77
+ data,
78
+ className,
79
+ height = 300,
80
+ onAction,
81
+ loading = false,
82
+ refreshing = false,
83
+ glassmorphism = false,
84
+ interactive = true
85
+ }: ChartWidgetProps) {
86
+ const [colorScheme, setColorScheme] = React.useState<keyof typeof CHART_COLORS>('default')
87
+ const [isFullscreen, setIsFullscreen] = React.useState(false)
88
+
89
+ const colors = CHART_COLORS[colorScheme]
90
+
91
+ // Custom tooltip
92
+ const CustomTooltip = ({ active, payload, label }: any) => {
93
+ if (!active || !payload) return null
94
+
95
+ return (
96
+ <motion.div
97
+ initial={{ opacity: 0, scale: 0.9 }}
98
+ animate={{ opacity: 1, scale: 1 }}
99
+ className={cn(
100
+ "p-3 rounded-lg shadow-lg",
101
+ glassmorphism
102
+ ? "bg-background/80 backdrop-blur-md border border-white/10"
103
+ : "bg-background border"
104
+ )}
105
+ >
106
+ <p className="text-sm font-medium">{label}</p>
107
+ {payload.map((entry: any, index: number) => (
108
+ <p key={index} className="text-sm mt-1">
109
+ <span
110
+ className="inline-block w-3 h-3 rounded-full mr-2"
111
+ style={{ backgroundColor: entry.color }}
112
+ />
113
+ {entry.name}: {entry.value}
114
+ </p>
115
+ ))}
116
+ </motion.div>
117
+ )
118
+ }
119
+
120
+ // Chart renderer
121
+ const renderChart = () => {
122
+ if (!data.data || data.data.length === 0) {
123
+ return (
124
+ <div className="flex items-center justify-center h-full text-muted-foreground">
125
+ No data available
126
+ </div>
127
+ )
128
+ }
129
+
130
+ const chartProps = {
131
+ data: data.data,
132
+ margin: { top: 5, right: 30, left: 20, bottom: 5 }
133
+ }
134
+
135
+ switch (data.type) {
136
+ case 'line':
137
+ return (
138
+ <ResponsiveContainer width="100%" height={height}>
139
+ <LineChart {...chartProps}>
140
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
141
+ <XAxis dataKey="name" />
142
+ <YAxis />
143
+ <Tooltip content={<CustomTooltip />} />
144
+ {interactive && <Legend />}
145
+ {Object.keys(data.data[0])
146
+ .filter(key => key !== 'name')
147
+ .map((key, index) => (
148
+ <Line
149
+ key={key}
150
+ type="monotone"
151
+ dataKey={key}
152
+ stroke={colors[index % colors.length]}
153
+ strokeWidth={2}
154
+ dot={{ r: 4 }}
155
+ activeDot={{ r: 6 }}
156
+ />
157
+ ))}
158
+ </LineChart>
159
+ </ResponsiveContainer>
160
+ )
161
+
162
+ case 'bar':
163
+ return (
164
+ <ResponsiveContainer width="100%" height={height}>
165
+ <BarChart {...chartProps}>
166
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
167
+ <XAxis dataKey="name" />
168
+ <YAxis />
169
+ <Tooltip content={<CustomTooltip />} />
170
+ {interactive && <Legend />}
171
+ {Object.keys(data.data[0])
172
+ .filter(key => key !== 'name')
173
+ .map((key, index) => (
174
+ <Bar
175
+ key={key}
176
+ dataKey={key}
177
+ fill={colors[index % colors.length]}
178
+ radius={[4, 4, 0, 0]}
179
+ />
180
+ ))}
181
+ </BarChart>
182
+ </ResponsiveContainer>
183
+ )
184
+
185
+ case 'area':
186
+ return (
187
+ <ResponsiveContainer width="100%" height={height}>
188
+ <AreaChart {...chartProps}>
189
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
190
+ <XAxis dataKey="name" />
191
+ <YAxis />
192
+ <Tooltip content={<CustomTooltip />} />
193
+ {interactive && <Legend />}
194
+ {Object.keys(data.data[0])
195
+ .filter(key => key !== 'name')
196
+ .map((key, index) => (
197
+ <Area
198
+ key={key}
199
+ type="monotone"
200
+ dataKey={key}
201
+ stroke={colors[index % colors.length]}
202
+ fill={colors[index % colors.length]}
203
+ fillOpacity={0.2}
204
+ />
205
+ ))}
206
+ </AreaChart>
207
+ </ResponsiveContainer>
208
+ )
209
+
210
+ case 'pie':
211
+ case 'donut':
212
+ return (
213
+ <ResponsiveContainer width="100%" height={height}>
214
+ <PieChart>
215
+ <Pie
216
+ data={data.data}
217
+ cx="50%"
218
+ cy="50%"
219
+ labelLine={false}
220
+ label={interactive}
221
+ outerRadius={data.type === 'donut' ? 80 : 100}
222
+ innerRadius={data.type === 'donut' ? 50 : 0}
223
+ fill="#8884d8"
224
+ dataKey="value"
225
+ >
226
+ {data.data.map((entry: any, index: number) => (
227
+ <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
228
+ ))}
229
+ </Pie>
230
+ <Tooltip content={<CustomTooltip />} />
231
+ {interactive && <Legend />}
232
+ </PieChart>
233
+ </ResponsiveContainer>
234
+ )
235
+
236
+ case 'radar':
237
+ return (
238
+ <ResponsiveContainer width="100%" height={height}>
239
+ <RadarChart data={data.data}>
240
+ <PolarGrid />
241
+ <PolarAngleAxis dataKey="subject" />
242
+ <PolarRadiusAxis />
243
+ {Object.keys(data.data[0])
244
+ .filter(key => key !== 'subject')
245
+ .map((key, index) => (
246
+ <Radar
247
+ key={key}
248
+ name={key}
249
+ dataKey={key}
250
+ stroke={colors[index % colors.length]}
251
+ fill={colors[index % colors.length]}
252
+ fillOpacity={0.3}
253
+ />
254
+ ))}
255
+ <Tooltip content={<CustomTooltip />} />
256
+ {interactive && <Legend />}
257
+ </RadarChart>
258
+ </ResponsiveContainer>
259
+ )
260
+
261
+ default:
262
+ return null
263
+ }
264
+ }
265
+
266
+ // Card variants
267
+ const cardVariants = {
268
+ initial: { opacity: 0, scale: 0.95 },
269
+ animate: {
270
+ opacity: 1,
271
+ scale: 1,
272
+ transition: { duration: 0.3, ease: "easeOut" }
273
+ }
274
+ }
275
+
276
+ return (
277
+ <motion.div
278
+ variants={cardVariants}
279
+ initial="initial"
280
+ animate="animate"
281
+ className={cn(isFullscreen && "fixed inset-4 z-50")}
282
+ >
283
+ <Card className={cn(
284
+ "relative overflow-hidden transition-all duration-300",
285
+ glassmorphism && "bg-background/60 backdrop-blur-md border-white/10",
286
+ isFullscreen && "h-full",
287
+ className
288
+ )}>
289
+ {/* Header */}
290
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
291
+ <div>
292
+ <CardTitle className="text-base font-semibold">{title}</CardTitle>
293
+ {description && (
294
+ <p className="text-sm text-muted-foreground mt-1">{description}</p>
295
+ )}
296
+ </div>
297
+
298
+ {/* Actions */}
299
+ <div className="flex items-center gap-2">
300
+ {refreshing && (
301
+ <motion.div
302
+ animate={{ rotate: 360 }}
303
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
304
+ >
305
+ <RefreshCw className="h-4 w-4 text-muted-foreground" />
306
+ </motion.div>
307
+ )}
308
+
309
+ {interactive && (
310
+ <DropdownMenu>
311
+ <DropdownMenuTrigger asChild>
312
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
313
+ <MoreVertical className="h-4 w-4" />
314
+ </Button>
315
+ </DropdownMenuTrigger>
316
+ <DropdownMenuContent align="end" className="w-48">
317
+ <DropdownMenuItem onClick={() => {
318
+ setIsFullscreen(!isFullscreen)
319
+ onAction?.('fullscreen', { fullscreen: !isFullscreen })
320
+ }}>
321
+ <Maximize2 className="mr-2 h-4 w-4" />
322
+ {isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
323
+ </DropdownMenuItem>
324
+
325
+ <DropdownMenuSub>
326
+ <DropdownMenuSubTrigger>
327
+ <Palette className="mr-2 h-4 w-4" />
328
+ Color Scheme
329
+ </DropdownMenuSubTrigger>
330
+ <DropdownMenuSubContent>
331
+ {Object.keys(CHART_COLORS).map((scheme) => (
332
+ <DropdownMenuItem
333
+ key={scheme}
334
+ onClick={() => setColorScheme(scheme as keyof typeof CHART_COLORS)}
335
+ >
336
+ <div className="flex items-center gap-2">
337
+ <div className="flex gap-1">
338
+ {CHART_COLORS[scheme as keyof typeof CHART_COLORS].slice(0, 3).map((color, i) => (
339
+ <div
340
+ key={i}
341
+ className="w-3 h-3 rounded-full"
342
+ style={{ backgroundColor: color }}
343
+ />
344
+ ))}
345
+ </div>
346
+ <span className="capitalize">{scheme}</span>
347
+ </div>
348
+ </DropdownMenuItem>
349
+ ))}
350
+ </DropdownMenuSubContent>
351
+ </DropdownMenuSub>
352
+
353
+ <DropdownMenuSeparator />
354
+
355
+ <DropdownMenuItem onClick={() => onAction?.('export', { format: 'png' })}>
356
+ <Download className="mr-2 h-4 w-4" />
357
+ Export as PNG
358
+ </DropdownMenuItem>
359
+
360
+ <DropdownMenuItem onClick={() => onAction?.('export', { format: 'csv' })}>
361
+ <Download className="mr-2 h-4 w-4" />
362
+ Export Data
363
+ </DropdownMenuItem>
364
+
365
+ <DropdownMenuSeparator />
366
+
367
+ <DropdownMenuItem onClick={() => onAction?.('filter')}>
368
+ <Filter className="mr-2 h-4 w-4" />
369
+ Filter Data
370
+ </DropdownMenuItem>
371
+
372
+ <DropdownMenuItem onClick={() => onAction?.('settings')}>
373
+ <Settings className="mr-2 h-4 w-4" />
374
+ Chart Settings
375
+ </DropdownMenuItem>
376
+ </DropdownMenuContent>
377
+ </DropdownMenu>
378
+ )}
379
+ </div>
380
+ </CardHeader>
381
+
382
+ {/* Chart Content */}
383
+ <CardContent className="p-0 px-4 pb-4">
384
+ <AnimatePresence mode="wait">
385
+ {loading ? (
386
+ <motion.div
387
+ key="loading"
388
+ initial={{ opacity: 0 }}
389
+ animate={{ opacity: 1 }}
390
+ exit={{ opacity: 0 }}
391
+ className="flex items-center justify-center"
392
+ style={{ height }}
393
+ >
394
+ <div className="space-y-2">
395
+ <div className="animate-pulse bg-muted h-4 w-32 rounded" />
396
+ <div className="animate-pulse bg-muted h-4 w-24 rounded" />
397
+ <div className="animate-pulse bg-muted h-4 w-28 rounded" />
398
+ </div>
399
+ </motion.div>
400
+ ) : (
401
+ <motion.div
402
+ key="chart"
403
+ initial={{ opacity: 0, y: 10 }}
404
+ animate={{ opacity: 1, y: 0 }}
405
+ exit={{ opacity: 0, y: -10 }}
406
+ transition={{ duration: 0.3 }}
407
+ >
408
+ {renderChart()}
409
+ </motion.div>
410
+ )}
411
+ </AnimatePresence>
412
+ </CardContent>
413
+ </Card>
414
+ </motion.div>
415
+ )
416
+ }
417
+
418
+ export default ChartWidget