@moontra/moonui-pro 2.0.22 → 2.0.23
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/package.json +2 -1
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +531 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +11 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-validator.tsx +183 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
|
4
|
+
import { cn } from '../../lib/utils'
|
|
5
|
+
|
|
6
|
+
// Virtual List Props
|
|
7
|
+
export interface VirtualListProps<T = any> {
|
|
8
|
+
items: T[]
|
|
9
|
+
height: number
|
|
10
|
+
itemHeight?: number
|
|
11
|
+
estimatedItemHeight?: number
|
|
12
|
+
variableHeight?: boolean
|
|
13
|
+
overscan?: number
|
|
14
|
+
renderItem: (item: T, index: number) => React.ReactNode
|
|
15
|
+
onScroll?: (scrollTop: number) => void
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Item bilgilerini saklamak için
|
|
20
|
+
interface ItemInfo {
|
|
21
|
+
index: number
|
|
22
|
+
height: number
|
|
23
|
+
top: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Performans optimize edilmiş Virtual List Component
|
|
27
|
+
export function VirtualList<T = any>({
|
|
28
|
+
items,
|
|
29
|
+
height,
|
|
30
|
+
itemHeight = 50,
|
|
31
|
+
estimatedItemHeight = 50,
|
|
32
|
+
variableHeight = false,
|
|
33
|
+
overscan = 5,
|
|
34
|
+
renderItem,
|
|
35
|
+
onScroll,
|
|
36
|
+
className
|
|
37
|
+
}: VirtualListProps<T>) {
|
|
38
|
+
// State yönetimi
|
|
39
|
+
const [scrollTop, setScrollTop] = useState(0)
|
|
40
|
+
const [isScrolling, setIsScrolling] = useState(false)
|
|
41
|
+
const [itemHeights, setItemHeights] = useState<Map<number, number>>(new Map())
|
|
42
|
+
|
|
43
|
+
// Refs
|
|
44
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
const scrollElementRef = useRef<HTMLDivElement>(null)
|
|
46
|
+
const scrollTimeoutRef = useRef<NodeJS.Timeout>()
|
|
47
|
+
const isScrollingTimeoutRef = useRef<NodeJS.Timeout>()
|
|
48
|
+
|
|
49
|
+
// Item pozisyonlarını hesapla
|
|
50
|
+
const itemPositions = useMemo(() => {
|
|
51
|
+
const positions: ItemInfo[] = []
|
|
52
|
+
let currentTop = 0
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < items.length; i++) {
|
|
55
|
+
const currentHeight = variableHeight
|
|
56
|
+
? (itemHeights.get(i) || estimatedItemHeight)
|
|
57
|
+
: itemHeight
|
|
58
|
+
|
|
59
|
+
positions.push({
|
|
60
|
+
index: i,
|
|
61
|
+
height: currentHeight,
|
|
62
|
+
top: currentTop
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
currentTop += currentHeight
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return positions
|
|
69
|
+
}, [items.length, itemHeight, estimatedItemHeight, variableHeight, itemHeights])
|
|
70
|
+
|
|
71
|
+
// Toplam içerik yüksekliği
|
|
72
|
+
const totalHeight = useMemo(() => {
|
|
73
|
+
return itemPositions.length > 0
|
|
74
|
+
? itemPositions[itemPositions.length - 1].top + itemPositions[itemPositions.length - 1].height
|
|
75
|
+
: 0
|
|
76
|
+
}, [itemPositions])
|
|
77
|
+
|
|
78
|
+
// Görünür aralığı hesapla
|
|
79
|
+
const visibleRange = useMemo(() => {
|
|
80
|
+
const start = itemPositions.findIndex(item => item.top + item.height >= scrollTop)
|
|
81
|
+
const end = itemPositions.findIndex(item => item.top > scrollTop + height)
|
|
82
|
+
|
|
83
|
+
const actualStart = Math.max(0, start - overscan)
|
|
84
|
+
const actualEnd = end === -1
|
|
85
|
+
? Math.min(items.length - 1, start + Math.ceil(height / itemHeight) + overscan)
|
|
86
|
+
: Math.min(items.length - 1, end + overscan)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
start: actualStart,
|
|
90
|
+
end: actualEnd
|
|
91
|
+
}
|
|
92
|
+
}, [scrollTop, height, itemPositions, overscan, items.length, itemHeight])
|
|
93
|
+
|
|
94
|
+
// Görünür itemlar
|
|
95
|
+
const visibleItems = useMemo(() => {
|
|
96
|
+
const result: Array<{ item: T; index: number; style: React.CSSProperties }> = []
|
|
97
|
+
|
|
98
|
+
for (let i = visibleRange.start; i <= visibleRange.end; i++) {
|
|
99
|
+
if (i >= 0 && i < items.length && itemPositions[i]) {
|
|
100
|
+
const position = itemPositions[i]
|
|
101
|
+
result.push({
|
|
102
|
+
item: items[i],
|
|
103
|
+
index: i,
|
|
104
|
+
style: {
|
|
105
|
+
position: 'absolute',
|
|
106
|
+
top: position.top,
|
|
107
|
+
left: 0,
|
|
108
|
+
right: 0,
|
|
109
|
+
height: variableHeight ? 'auto' : position.height,
|
|
110
|
+
minHeight: variableHeight ? position.height : undefined
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return result
|
|
117
|
+
}, [items, visibleRange, itemPositions, variableHeight])
|
|
118
|
+
|
|
119
|
+
// Scroll handler
|
|
120
|
+
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
121
|
+
const scrollTop = e.currentTarget.scrollTop
|
|
122
|
+
setScrollTop(scrollTop)
|
|
123
|
+
setIsScrolling(true)
|
|
124
|
+
|
|
125
|
+
// Scroll callback
|
|
126
|
+
if (onScroll) {
|
|
127
|
+
onScroll(scrollTop)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Scroll timeout'ını temizle ve yenisini ayarla
|
|
131
|
+
if (scrollTimeoutRef.current) {
|
|
132
|
+
clearTimeout(scrollTimeoutRef.current)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (isScrollingTimeoutRef.current) {
|
|
136
|
+
clearTimeout(isScrollingTimeoutRef.current)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 150ms sonra scrolling'i false yap (performans için)
|
|
140
|
+
isScrollingTimeoutRef.current = setTimeout(() => {
|
|
141
|
+
setIsScrolling(false)
|
|
142
|
+
}, 150)
|
|
143
|
+
}, [onScroll])
|
|
144
|
+
|
|
145
|
+
// Variable height için resize observer
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!variableHeight || !containerRef.current) return
|
|
148
|
+
|
|
149
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
150
|
+
const newHeights = new Map(itemHeights)
|
|
151
|
+
let hasChanges = false
|
|
152
|
+
|
|
153
|
+
entries.forEach((entry) => {
|
|
154
|
+
const element = entry.target as HTMLElement
|
|
155
|
+
const indexAttr = element.getAttribute('data-index')
|
|
156
|
+
|
|
157
|
+
if (indexAttr) {
|
|
158
|
+
const index = parseInt(indexAttr, 10)
|
|
159
|
+
const newHeight = entry.contentRect.height
|
|
160
|
+
|
|
161
|
+
if (newHeights.get(index) !== newHeight) {
|
|
162
|
+
newHeights.set(index, newHeight)
|
|
163
|
+
hasChanges = true
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
if (hasChanges) {
|
|
169
|
+
setItemHeights(newHeights)
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Mevcut tüm item elementlerini observe et
|
|
174
|
+
const itemElements = containerRef.current.querySelectorAll('[data-index]')
|
|
175
|
+
itemElements.forEach(el => resizeObserver.observe(el))
|
|
176
|
+
|
|
177
|
+
return () => resizeObserver.disconnect()
|
|
178
|
+
}, [variableHeight, itemHeights, visibleItems])
|
|
179
|
+
|
|
180
|
+
// Keyboard navigation desteği
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
183
|
+
if (!containerRef.current) return
|
|
184
|
+
|
|
185
|
+
switch (e.key) {
|
|
186
|
+
case 'ArrowUp':
|
|
187
|
+
e.preventDefault()
|
|
188
|
+
scrollElementRef.current?.scrollBy({ top: -itemHeight, behavior: 'smooth' })
|
|
189
|
+
break
|
|
190
|
+
case 'ArrowDown':
|
|
191
|
+
e.preventDefault()
|
|
192
|
+
scrollElementRef.current?.scrollBy({ top: itemHeight, behavior: 'smooth' })
|
|
193
|
+
break
|
|
194
|
+
case 'PageUp':
|
|
195
|
+
e.preventDefault()
|
|
196
|
+
scrollElementRef.current?.scrollBy({ top: -height, behavior: 'smooth' })
|
|
197
|
+
break
|
|
198
|
+
case 'PageDown':
|
|
199
|
+
e.preventDefault()
|
|
200
|
+
scrollElementRef.current?.scrollBy({ top: height, behavior: 'smooth' })
|
|
201
|
+
break
|
|
202
|
+
case 'Home':
|
|
203
|
+
e.preventDefault()
|
|
204
|
+
scrollElementRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
205
|
+
break
|
|
206
|
+
case 'End':
|
|
207
|
+
e.preventDefault()
|
|
208
|
+
scrollElementRef.current?.scrollTo({ top: totalHeight, behavior: 'smooth' })
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const container = containerRef.current
|
|
214
|
+
if (container) {
|
|
215
|
+
container.addEventListener('keydown', handleKeyDown)
|
|
216
|
+
return () => container.removeEventListener('keydown', handleKeyDown)
|
|
217
|
+
}
|
|
218
|
+
}, [itemHeight, height, totalHeight])
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div
|
|
222
|
+
ref={containerRef}
|
|
223
|
+
className={cn(
|
|
224
|
+
"relative overflow-hidden border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-ring",
|
|
225
|
+
className
|
|
226
|
+
)}
|
|
227
|
+
style={{ height }}
|
|
228
|
+
tabIndex={0}
|
|
229
|
+
role="listbox"
|
|
230
|
+
aria-label={`Virtual list with ${items.length} items`}
|
|
231
|
+
>
|
|
232
|
+
<div
|
|
233
|
+
ref={scrollElementRef}
|
|
234
|
+
className="h-full overflow-auto scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent"
|
|
235
|
+
onScroll={handleScroll}
|
|
236
|
+
>
|
|
237
|
+
<div
|
|
238
|
+
style={{
|
|
239
|
+
height: totalHeight,
|
|
240
|
+
position: 'relative'
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
{visibleItems.map(({ item, index, style }) => (
|
|
244
|
+
<div
|
|
245
|
+
key={index}
|
|
246
|
+
data-index={index}
|
|
247
|
+
style={style}
|
|
248
|
+
role="option"
|
|
249
|
+
aria-selected="false"
|
|
250
|
+
className="outline-none"
|
|
251
|
+
>
|
|
252
|
+
{renderItem(item, index)}
|
|
253
|
+
</div>
|
|
254
|
+
))}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* Loading indicator for scrolling */}
|
|
259
|
+
{isScrolling && (
|
|
260
|
+
<div className="absolute top-2 right-2 bg-muted/80 text-muted-foreground px-2 py-1 rounded text-xs">
|
|
261
|
+
Scrolling...
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Scroll position indicator */}
|
|
266
|
+
{items.length > 100 && (
|
|
267
|
+
<div className="absolute bottom-2 right-2 bg-muted/80 text-muted-foreground px-2 py-1 rounded text-xs">
|
|
268
|
+
{Math.round((scrollTop / Math.max(totalHeight - height, 1)) * 100)}%
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Utility hook for virtual list state management
|
|
276
|
+
export function useVirtualList<T>(
|
|
277
|
+
items: T[],
|
|
278
|
+
initialConfig: Partial<VirtualListProps<T>> = {}
|
|
279
|
+
) {
|
|
280
|
+
const [config, setConfig] = useState(initialConfig)
|
|
281
|
+
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set())
|
|
282
|
+
|
|
283
|
+
const updateConfig = useCallback((newConfig: Partial<VirtualListProps<T>>) => {
|
|
284
|
+
setConfig(prev => ({ ...prev, ...newConfig }))
|
|
285
|
+
}, [])
|
|
286
|
+
|
|
287
|
+
const selectItem = useCallback((index: number) => {
|
|
288
|
+
setSelectedItems(prev => new Set([...prev, index]))
|
|
289
|
+
}, [])
|
|
290
|
+
|
|
291
|
+
const deselectItem = useCallback((index: number) => {
|
|
292
|
+
setSelectedItems(prev => {
|
|
293
|
+
const newSet = new Set(prev)
|
|
294
|
+
newSet.delete(index)
|
|
295
|
+
return newSet
|
|
296
|
+
})
|
|
297
|
+
}, [])
|
|
298
|
+
|
|
299
|
+
const clearSelection = useCallback(() => {
|
|
300
|
+
setSelectedItems(new Set())
|
|
301
|
+
}, [])
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
config,
|
|
305
|
+
updateConfig,
|
|
306
|
+
selectedItems,
|
|
307
|
+
selectItem,
|
|
308
|
+
deselectItem,
|
|
309
|
+
clearSelection
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Enhanced Virtual List with selection support
|
|
314
|
+
export interface SelectableVirtualListProps<T = any> extends VirtualListProps<T> {
|
|
315
|
+
selectable?: boolean
|
|
316
|
+
multiSelect?: boolean
|
|
317
|
+
selectedItems?: Set<number>
|
|
318
|
+
onSelectionChange?: (selectedItems: Set<number>) => void
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function SelectableVirtualList<T = any>({
|
|
322
|
+
selectable = false,
|
|
323
|
+
multiSelect = false,
|
|
324
|
+
selectedItems = new Set(),
|
|
325
|
+
onSelectionChange,
|
|
326
|
+
renderItem: originalRenderItem,
|
|
327
|
+
...props
|
|
328
|
+
}: SelectableVirtualListProps<T>) {
|
|
329
|
+
const handleItemClick = useCallback((index: number) => {
|
|
330
|
+
if (!selectable || !onSelectionChange) return
|
|
331
|
+
|
|
332
|
+
const newSelection = new Set(selectedItems)
|
|
333
|
+
|
|
334
|
+
if (multiSelect) {
|
|
335
|
+
if (newSelection.has(index)) {
|
|
336
|
+
newSelection.delete(index)
|
|
337
|
+
} else {
|
|
338
|
+
newSelection.add(index)
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
newSelection.clear()
|
|
342
|
+
newSelection.add(index)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
onSelectionChange(newSelection)
|
|
346
|
+
}, [selectable, multiSelect, selectedItems, onSelectionChange])
|
|
347
|
+
|
|
348
|
+
const renderItem = useCallback((item: T, index: number) => {
|
|
349
|
+
const isSelected = selectedItems.has(index)
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div
|
|
353
|
+
className={cn(
|
|
354
|
+
"transition-colors",
|
|
355
|
+
selectable && "cursor-pointer hover:bg-muted/50",
|
|
356
|
+
isSelected && "bg-primary/10 border-l-4 border-primary"
|
|
357
|
+
)}
|
|
358
|
+
onClick={() => selectable && handleItemClick(index)}
|
|
359
|
+
>
|
|
360
|
+
{originalRenderItem(item, index)}
|
|
361
|
+
</div>
|
|
362
|
+
)
|
|
363
|
+
}, [originalRenderItem, selectable, selectedItems, handleItemClick])
|
|
364
|
+
|
|
365
|
+
return <VirtualList {...props} renderItem={renderItem} />
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Export types
|
|
369
|
+
export type { VirtualListProps, SelectableVirtualListProps }
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { ChartDataPoint, ChartSeries } from '../components/advanced-chart'
|
|
5
|
+
|
|
6
|
+
export interface UseChartOptions {
|
|
7
|
+
data: ChartDataPoint[]
|
|
8
|
+
series: ChartSeries[]
|
|
9
|
+
realtime?: boolean
|
|
10
|
+
refreshInterval?: number
|
|
11
|
+
maxDataPoints?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseChartReturn {
|
|
15
|
+
data: ChartDataPoint[]
|
|
16
|
+
series: ChartSeries[]
|
|
17
|
+
isLoading: boolean
|
|
18
|
+
error: string | null
|
|
19
|
+
addDataPoint: (point: ChartDataPoint) => void
|
|
20
|
+
updateSeries: (seriesUpdate: Partial<ChartSeries>[]) => void
|
|
21
|
+
toggleSeries: (dataKey: string) => void
|
|
22
|
+
resetData: () => void
|
|
23
|
+
exportData: (format: 'csv' | 'json') => void
|
|
24
|
+
getStats: () => {
|
|
25
|
+
total: number
|
|
26
|
+
average: number
|
|
27
|
+
min: number
|
|
28
|
+
max: number
|
|
29
|
+
trend: 'up' | 'down' | 'neutral'
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useChart({
|
|
34
|
+
data: initialData,
|
|
35
|
+
series: initialSeries,
|
|
36
|
+
realtime = false,
|
|
37
|
+
refreshInterval = 5000,
|
|
38
|
+
maxDataPoints = 100,
|
|
39
|
+
}: UseChartOptions): UseChartReturn {
|
|
40
|
+
const [data, setData] = React.useState<ChartDataPoint[]>(initialData)
|
|
41
|
+
const [series, setSeries] = React.useState<ChartSeries[]>(initialSeries)
|
|
42
|
+
const [isLoading, setIsLoading] = React.useState(false)
|
|
43
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
44
|
+
|
|
45
|
+
// Realtime data updates
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (!realtime) return
|
|
48
|
+
|
|
49
|
+
const interval = setInterval(() => {
|
|
50
|
+
// This would typically fetch new data from an API
|
|
51
|
+
// For demo purposes, we'll simulate data updates
|
|
52
|
+
setData(prevData => {
|
|
53
|
+
const newData = [...prevData]
|
|
54
|
+
|
|
55
|
+
// Keep only the last maxDataPoints
|
|
56
|
+
if (newData.length >= maxDataPoints) {
|
|
57
|
+
newData.shift()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Add simulated new data point
|
|
61
|
+
const lastPoint = newData[newData.length - 1]
|
|
62
|
+
if (lastPoint) {
|
|
63
|
+
const newPoint: ChartDataPoint = { ...lastPoint }
|
|
64
|
+
|
|
65
|
+
// Simulate data changes for each series
|
|
66
|
+
series.forEach(s => {
|
|
67
|
+
if (typeof lastPoint[s.dataKey] === 'number') {
|
|
68
|
+
const currentValue = lastPoint[s.dataKey] as number
|
|
69
|
+
const change = (Math.random() - 0.5) * currentValue * 0.1
|
|
70
|
+
newPoint[s.dataKey] = Math.max(0, currentValue + change)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// Update timestamp if exists
|
|
75
|
+
if ('timestamp' in newPoint) {
|
|
76
|
+
newPoint.timestamp = Date.now()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
newData.push(newPoint)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return newData
|
|
83
|
+
})
|
|
84
|
+
}, refreshInterval)
|
|
85
|
+
|
|
86
|
+
return () => clearInterval(interval)
|
|
87
|
+
}, [realtime, refreshInterval, maxDataPoints, series])
|
|
88
|
+
|
|
89
|
+
const addDataPoint = React.useCallback((point: ChartDataPoint) => {
|
|
90
|
+
setData(prevData => {
|
|
91
|
+
const newData = [...prevData, point]
|
|
92
|
+
|
|
93
|
+
// Keep only the last maxDataPoints
|
|
94
|
+
if (newData.length > maxDataPoints) {
|
|
95
|
+
return newData.slice(-maxDataPoints)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return newData
|
|
99
|
+
})
|
|
100
|
+
}, [maxDataPoints])
|
|
101
|
+
|
|
102
|
+
const updateSeries = React.useCallback((seriesUpdate: Partial<ChartSeries>[]) => {
|
|
103
|
+
setSeries(prevSeries => {
|
|
104
|
+
return prevSeries.map(s => {
|
|
105
|
+
const update = seriesUpdate.find(u => u.dataKey === s.dataKey)
|
|
106
|
+
return update ? { ...s, ...update } : s
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
}, [])
|
|
110
|
+
|
|
111
|
+
const toggleSeries = React.useCallback((dataKey: string) => {
|
|
112
|
+
setSeries(prevSeries => {
|
|
113
|
+
return prevSeries.map(s =>
|
|
114
|
+
s.dataKey === dataKey ? { ...s, hide: !s.hide } : s
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
}, [])
|
|
118
|
+
|
|
119
|
+
const resetData = React.useCallback(() => {
|
|
120
|
+
setData(initialData)
|
|
121
|
+
setSeries(initialSeries)
|
|
122
|
+
setError(null)
|
|
123
|
+
}, [initialData, initialSeries])
|
|
124
|
+
|
|
125
|
+
const exportData = React.useCallback((format: 'csv' | 'json') => {
|
|
126
|
+
try {
|
|
127
|
+
if (format === 'csv') {
|
|
128
|
+
const headers = Object.keys(data[0] || {})
|
|
129
|
+
const csvContent = [
|
|
130
|
+
headers.join(','),
|
|
131
|
+
...data.map(row =>
|
|
132
|
+
headers.map(header => {
|
|
133
|
+
const value = row[header]
|
|
134
|
+
if (typeof value === 'string' && (value.includes(',') || value.includes('"'))) {
|
|
135
|
+
return `"${value.replace(/"/g, '""')}"`
|
|
136
|
+
}
|
|
137
|
+
return value
|
|
138
|
+
}).join(',')
|
|
139
|
+
)
|
|
140
|
+
].join('\n')
|
|
141
|
+
|
|
142
|
+
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
|
143
|
+
const link = document.createElement('a')
|
|
144
|
+
link.href = URL.createObjectURL(blob)
|
|
145
|
+
link.download = 'chart-data.csv'
|
|
146
|
+
link.click()
|
|
147
|
+
} else if (format === 'json') {
|
|
148
|
+
const jsonContent = JSON.stringify({ data, series }, null, 2)
|
|
149
|
+
const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' })
|
|
150
|
+
const link = document.createElement('a')
|
|
151
|
+
link.href = URL.createObjectURL(blob)
|
|
152
|
+
link.download = 'chart-data.json'
|
|
153
|
+
link.click()
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
setError('Failed to export data')
|
|
157
|
+
}
|
|
158
|
+
}, [data, series])
|
|
159
|
+
|
|
160
|
+
const getStats = React.useCallback(() => {
|
|
161
|
+
if (!data.length || !series.length) {
|
|
162
|
+
return { total: 0, average: 0, min: 0, max: 0, trend: 'neutral' as const }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const firstSeries = series[0]
|
|
166
|
+
const values = data
|
|
167
|
+
.map(d => Number(d[firstSeries.dataKey]))
|
|
168
|
+
.filter(v => !isNaN(v))
|
|
169
|
+
|
|
170
|
+
if (!values.length) {
|
|
171
|
+
return { total: 0, average: 0, min: 0, max: 0, trend: 'neutral' as const }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const total = values.reduce((sum, val) => sum + val, 0)
|
|
175
|
+
const average = total / values.length
|
|
176
|
+
const min = Math.min(...values)
|
|
177
|
+
const max = Math.max(...values)
|
|
178
|
+
|
|
179
|
+
// Calculate trend
|
|
180
|
+
let trend: 'up' | 'down' | 'neutral' = 'neutral'
|
|
181
|
+
if (values.length >= 2) {
|
|
182
|
+
const first = values[0]
|
|
183
|
+
const last = values[values.length - 1]
|
|
184
|
+
const change = ((last - first) / first) * 100
|
|
185
|
+
|
|
186
|
+
if (change > 5) trend = 'up'
|
|
187
|
+
else if (change < -5) trend = 'down'
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { total, average, min, max, trend }
|
|
191
|
+
}, [data, series])
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
data,
|
|
195
|
+
series,
|
|
196
|
+
isLoading,
|
|
197
|
+
error,
|
|
198
|
+
addDataPoint,
|
|
199
|
+
updateSeries,
|
|
200
|
+
toggleSeries,
|
|
201
|
+
resetData,
|
|
202
|
+
exportData,
|
|
203
|
+
getStats,
|
|
204
|
+
}
|
|
205
|
+
}
|