@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.
Files changed (96) hide show
  1. package/package.json +2 -1
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  6. package/src/components/advanced-chart/index.tsx +412 -0
  7. package/src/components/advanced-forms/index.tsx +431 -0
  8. package/src/components/animated-button/index.tsx +202 -0
  9. package/src/components/calendar/event-dialog.tsx +372 -0
  10. package/src/components/calendar/index.tsx +531 -0
  11. package/src/components/color-picker/index.tsx +434 -0
  12. package/src/components/dashboard/index.tsx +334 -0
  13. package/src/components/data-table/data-table.test.tsx +187 -0
  14. package/src/components/data-table/index.tsx +368 -0
  15. package/src/components/draggable-list/index.tsx +100 -0
  16. package/src/components/enhanced/button.tsx +360 -0
  17. package/src/components/enhanced/card.tsx +272 -0
  18. package/src/components/enhanced/dialog.tsx +248 -0
  19. package/src/components/enhanced/index.ts +3 -0
  20. package/src/components/error-boundary/index.tsx +111 -0
  21. package/src/components/file-upload/file-upload.test.tsx +242 -0
  22. package/src/components/file-upload/index.tsx +362 -0
  23. package/src/components/floating-action-button/index.tsx +209 -0
  24. package/src/components/github-stars/index.tsx +414 -0
  25. package/src/components/health-check/index.tsx +441 -0
  26. package/src/components/hover-card-3d/index.tsx +170 -0
  27. package/src/components/index.ts +76 -0
  28. package/src/components/kanban/index.tsx +436 -0
  29. package/src/components/lazy-component/index.tsx +342 -0
  30. package/src/components/magnetic-button/index.tsx +170 -0
  31. package/src/components/memory-efficient-data/index.tsx +352 -0
  32. package/src/components/optimized-image/index.tsx +427 -0
  33. package/src/components/performance-debugger/index.tsx +591 -0
  34. package/src/components/performance-monitor/index.tsx +775 -0
  35. package/src/components/pinch-zoom/index.tsx +172 -0
  36. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  37. package/src/components/rich-text-editor/index.tsx +1537 -0
  38. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  39. package/src/components/rich-text-editor/slash-commands.css +35 -0
  40. package/src/components/rich-text-editor/table-styles.css +65 -0
  41. package/src/components/spotlight-card/index.tsx +194 -0
  42. package/src/components/swipeable-card/index.tsx +100 -0
  43. package/src/components/timeline/index.tsx +333 -0
  44. package/src/components/ui/animated-button.tsx +185 -0
  45. package/src/components/ui/avatar.tsx +135 -0
  46. package/src/components/ui/badge.tsx +225 -0
  47. package/src/components/ui/button.tsx +221 -0
  48. package/src/components/ui/card.tsx +141 -0
  49. package/src/components/ui/checkbox.tsx +256 -0
  50. package/src/components/ui/color-picker.tsx +95 -0
  51. package/src/components/ui/dialog.tsx +332 -0
  52. package/src/components/ui/dropdown-menu.tsx +200 -0
  53. package/src/components/ui/hover-card-3d.tsx +103 -0
  54. package/src/components/ui/index.ts +33 -0
  55. package/src/components/ui/input.tsx +219 -0
  56. package/src/components/ui/label.tsx +26 -0
  57. package/src/components/ui/magnetic-button.tsx +129 -0
  58. package/src/components/ui/popover.tsx +183 -0
  59. package/src/components/ui/select.tsx +273 -0
  60. package/src/components/ui/separator.tsx +140 -0
  61. package/src/components/ui/slider.tsx +351 -0
  62. package/src/components/ui/spotlight-card.tsx +119 -0
  63. package/src/components/ui/switch.tsx +83 -0
  64. package/src/components/ui/tabs.tsx +195 -0
  65. package/src/components/ui/textarea.tsx +25 -0
  66. package/src/components/ui/toast.tsx +313 -0
  67. package/src/components/ui/tooltip.tsx +152 -0
  68. package/src/components/virtual-list/index.tsx +369 -0
  69. package/src/hooks/use-chart.ts +205 -0
  70. package/src/hooks/use-data-table.ts +182 -0
  71. package/src/hooks/use-docs-pro-access.ts +13 -0
  72. package/src/hooks/use-license-check.ts +65 -0
  73. package/src/hooks/use-subscription.ts +19 -0
  74. package/src/index.ts +11 -0
  75. package/src/lib/micro-interactions.ts +255 -0
  76. package/src/lib/utils.ts +6 -0
  77. package/src/patterns/login-form/index.tsx +276 -0
  78. package/src/patterns/login-form/types.ts +67 -0
  79. package/src/setupTests.ts +41 -0
  80. package/src/styles/design-system.css +365 -0
  81. package/src/styles/index.css +4 -0
  82. package/src/styles/tailwind.css +6 -0
  83. package/src/styles/tokens.css +453 -0
  84. package/src/types/moonui.d.ts +22 -0
  85. package/src/use-intersection-observer.tsx +154 -0
  86. package/src/use-local-storage.tsx +71 -0
  87. package/src/use-paddle.ts +138 -0
  88. package/src/use-performance-optimizer.ts +379 -0
  89. package/src/use-pro-access.ts +141 -0
  90. package/src/use-scroll-animation.ts +221 -0
  91. package/src/use-subscription.ts +37 -0
  92. package/src/use-toast.ts +32 -0
  93. package/src/utils/chart-helpers.ts +257 -0
  94. package/src/utils/cn.ts +69 -0
  95. package/src/utils/data-processing.ts +151 -0
  96. package/src/utils/license-validator.tsx +183 -0
@@ -0,0 +1,221 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { useInView, useScroll, useTransform } from 'framer-motion'
5
+
6
+ interface UseScrollAnimationOptions {
7
+ threshold?: number
8
+ triggerOnce?: boolean
9
+ rootMargin?: string
10
+ }
11
+
12
+ export function useScrollAnimation(options: UseScrollAnimationOptions = {}) {
13
+ const ref = useRef<HTMLElement>(null)
14
+ const isInView = useInView(ref, {
15
+ threshold: options.threshold || 0.1,
16
+ once: options.triggerOnce ?? true,
17
+ margin: options.rootMargin || '-100px',
18
+ })
19
+
20
+ return { ref, isInView }
21
+ }
22
+
23
+ export function useScrollProgress() {
24
+ const { scrollYProgress } = useScroll()
25
+ return scrollYProgress
26
+ }
27
+
28
+ export function useScrollBasedAnimation() {
29
+ const { scrollY } = useScroll()
30
+ const y = useTransform(scrollY, [0, 300], [0, -50])
31
+ const opacity = useTransform(scrollY, [0, 300], [1, 0])
32
+ const scale = useTransform(scrollY, [0, 300], [1, 0.8])
33
+
34
+ return { y, opacity, scale, scrollY }
35
+ }
36
+
37
+ export function useParallaxScroll(speed: number = 0.5) {
38
+ const ref = useRef<HTMLElement>(null)
39
+ const { scrollYProgress } = useScroll({
40
+ target: ref,
41
+ offset: ['start end', 'end start']
42
+ })
43
+
44
+ const y = useTransform(scrollYProgress, [0, 1], [`-${speed * 100}%`, `${speed * 100}%`])
45
+
46
+ return { ref, y }
47
+ }
48
+
49
+ export function useScrollDirection() {
50
+ const [scrollDirection, setScrollDirection] = useState<'up' | 'down'>('down')
51
+ const [scrollY, setScrollY] = useState(0)
52
+
53
+ useEffect(() => {
54
+ let ticking = false
55
+
56
+ const updateScrollDirection = () => {
57
+ const newScrollY = window.scrollY
58
+
59
+ if (Math.abs(newScrollY - scrollY) < 5) {
60
+ ticking = false
61
+ return
62
+ }
63
+
64
+ setScrollDirection(newScrollY > scrollY ? 'down' : 'up')
65
+ setScrollY(newScrollY)
66
+ ticking = false
67
+ }
68
+
69
+ const handleScroll = () => {
70
+ if (!ticking) {
71
+ requestAnimationFrame(updateScrollDirection)
72
+ ticking = true
73
+ }
74
+ }
75
+
76
+ window.addEventListener('scroll', handleScroll)
77
+ return () => window.removeEventListener('scroll', handleScroll)
78
+ }, [scrollY])
79
+
80
+ return { scrollDirection, scrollY }
81
+ }
82
+
83
+ export function useScrollToElement() {
84
+ const scrollToElement = (elementId: string, offset: number = 0) => {
85
+ const element = document.getElementById(elementId)
86
+ if (element) {
87
+ const elementPosition = element.offsetTop - offset
88
+ window.scrollTo({
89
+ top: elementPosition,
90
+ behavior: 'smooth'
91
+ })
92
+ }
93
+ }
94
+
95
+ return { scrollToElement }
96
+ }
97
+
98
+ interface UseInfiniteScrollOptions {
99
+ threshold?: number
100
+ rootMargin?: string
101
+ }
102
+
103
+ export function useInfiniteScroll(
104
+ callback: () => void,
105
+ options: UseInfiniteScrollOptions = {}
106
+ ) {
107
+ const ref = useRef<HTMLElement>(null)
108
+ const isInView = useInView(ref, {
109
+ threshold: options.threshold || 0.1,
110
+ margin: options.rootMargin || '100px',
111
+ })
112
+
113
+ useEffect(() => {
114
+ if (isInView) {
115
+ callback()
116
+ }
117
+ }, [isInView, callback])
118
+
119
+ return ref
120
+ }
121
+
122
+ export function useScrollBasedScale() {
123
+ const ref = useRef<HTMLElement>(null)
124
+ const { scrollYProgress } = useScroll({
125
+ target: ref,
126
+ offset: ['start end', 'end start']
127
+ })
128
+
129
+ const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8])
130
+ const opacity = useTransform(scrollYProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0])
131
+
132
+ return { ref, scale, opacity }
133
+ }
134
+
135
+ export function useScrollBasedRotation() {
136
+ const ref = useRef<HTMLElement>(null)
137
+ const { scrollYProgress } = useScroll({
138
+ target: ref,
139
+ offset: ['start end', 'end start']
140
+ })
141
+
142
+ const rotate = useTransform(scrollYProgress, [0, 1], [0, 360])
143
+
144
+ return { ref, rotate }
145
+ }
146
+
147
+ export function useScrollTriggeredCounter(
148
+ endValue: number,
149
+ duration: number = 2000
150
+ ) {
151
+ const [count, setCount] = useState(0)
152
+ const [isVisible, setIsVisible] = useState(false)
153
+ const ref = useRef<HTMLElement>(null)
154
+
155
+ const isInView = useInView(ref, {
156
+ threshold: 0.5,
157
+ once: true
158
+ })
159
+
160
+ useEffect(() => {
161
+ if (isInView && !isVisible) {
162
+ setIsVisible(true)
163
+ let startTime: number
164
+
165
+ const animate = (currentTime: number) => {
166
+ if (!startTime) startTime = currentTime
167
+ const elapsed = currentTime - startTime
168
+ const progress = Math.min(elapsed / duration, 1)
169
+
170
+ // Easing function for smooth animation
171
+ const easeOutQuart = 1 - Math.pow(1 - progress, 4)
172
+ setCount(Math.floor(easeOutQuart * endValue))
173
+
174
+ if (progress < 1) {
175
+ requestAnimationFrame(animate)
176
+ }
177
+ }
178
+
179
+ requestAnimationFrame(animate)
180
+ }
181
+ }, [isInView, endValue, duration, isVisible])
182
+
183
+ return { ref, count }
184
+ }
185
+
186
+ export function useScrollBasedBlur() {
187
+ const { scrollY } = useScroll()
188
+ const blur = useTransform(scrollY, [0, 300], [0, 10])
189
+
190
+ return blur
191
+ }
192
+
193
+ export function useScrollSnapPoints(snapPoints: number[]) {
194
+ const [currentSnap, setCurrentSnap] = useState(0)
195
+
196
+ useEffect(() => {
197
+ const handleScroll = () => {
198
+ const scrollPosition = window.scrollY
199
+ const closest = snapPoints.reduce((prev, curr, index) => {
200
+ return Math.abs(curr - scrollPosition) < Math.abs(snapPoints[prev] - scrollPosition)
201
+ ? index
202
+ : prev
203
+ }, 0)
204
+ setCurrentSnap(closest)
205
+ }
206
+
207
+ window.addEventListener('scroll', handleScroll)
208
+ return () => window.removeEventListener('scroll', handleScroll)
209
+ }, [snapPoints])
210
+
211
+ const scrollToSnap = (index: number) => {
212
+ if (index >= 0 && index < snapPoints.length) {
213
+ window.scrollTo({
214
+ top: snapPoints[index],
215
+ behavior: 'smooth'
216
+ })
217
+ }
218
+ }
219
+
220
+ return { currentSnap, scrollToSnap }
221
+ }
@@ -0,0 +1,37 @@
1
+ import { useSession } from "next-auth/react";
2
+
3
+ export function useSubscription() {
4
+ const { data: session, status } = useSession();
5
+
6
+ const isLoading = status === "loading";
7
+ const isAuthenticated = status === "authenticated";
8
+
9
+ // Admin kullanıcılar her zaman pro erişime sahip
10
+ const isAdmin = session?.user?.role === "admin";
11
+
12
+ // Pro abonelik kontrolü
13
+ const hasProAccess = isAdmin || session?.user?.subscription?.status === "active";
14
+ const subscriptionPlan = session?.user?.subscription?.plan || (isAdmin ? "lifetime" : "free");
15
+
16
+ // Debug bilgisi
17
+ if (process.env.NODE_ENV === 'development') {
18
+ console.log('🔍 useSubscription Debug:', {
19
+ email: session?.user?.email,
20
+ role: session?.user?.role,
21
+ isAdmin,
22
+ subscription: session?.user?.subscription,
23
+ hasProAccess,
24
+ subscriptionPlan,
25
+ status
26
+ });
27
+ }
28
+
29
+ return {
30
+ isLoading,
31
+ isAuthenticated,
32
+ isAdmin,
33
+ hasProAccess,
34
+ subscriptionPlan,
35
+ subscription: session?.user?.subscription,
36
+ };
37
+ }
@@ -0,0 +1,32 @@
1
+ // Toast hook - şimdilik basit bir implementasyon
2
+ // Gerçek implementasyonda Toaster component'i ile entegre olacak
3
+
4
+ interface ToastOptions {
5
+ title: string;
6
+ description?: string;
7
+ variant?: 'default' | 'destructive';
8
+ }
9
+
10
+ export function toast(options: ToastOptions) {
11
+ // Şimdilik console'a yazdıralım
12
+ // Gerçek implementasyonda toast notification gösterilecek
13
+ console.log('Toast:', options);
14
+
15
+ // Browser'da alert gösterelim (geçici çözüm)
16
+ if (typeof window !== 'undefined') {
17
+ const message = options.description
18
+ ? `${options.title}\n\n${options.description}`
19
+ : options.title;
20
+
21
+ // Variant'a göre stil belirle
22
+ if (options.variant === 'destructive') {
23
+ console.error(message);
24
+ } else {
25
+ console.log(message);
26
+ }
27
+ }
28
+ }
29
+
30
+ export const useToast = () => {
31
+ return { toast };
32
+ };
@@ -0,0 +1,257 @@
1
+ import { ChartDataPoint, ChartSeries } from '../components/advanced-chart'
2
+
3
+ export interface ChartTheme {
4
+ colors: string[]
5
+ backgroundColor: string
6
+ textColor: string
7
+ gridColor: string
8
+ tooltipBackground: string
9
+ tooltipBorder: string
10
+ }
11
+
12
+ export const CHART_THEMES: Record<string, ChartTheme> = {
13
+ default: {
14
+ colors: ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4', '#f97316', '#84cc16', '#ec4899', '#6366f1'],
15
+ backgroundColor: '#ffffff',
16
+ textColor: '#374151',
17
+ gridColor: '#e5e7eb',
18
+ tooltipBackground: '#1f2937',
19
+ tooltipBorder: '#374151',
20
+ },
21
+ dark: {
22
+ colors: ['#60a5fa', '#f87171', '#34d399', '#fbbf24', '#a78bfa', '#22d3ee', '#fb923c', '#a3e635', '#f472b6', '#818cf8'],
23
+ backgroundColor: '#1f2937',
24
+ textColor: '#f9fafb',
25
+ gridColor: '#374151',
26
+ tooltipBackground: '#374151',
27
+ tooltipBorder: '#4b5563',
28
+ },
29
+ minimal: {
30
+ colors: ['#000000', '#666666', '#999999', '#cccccc'],
31
+ backgroundColor: '#ffffff',
32
+ textColor: '#000000',
33
+ gridColor: '#f3f4f6',
34
+ tooltipBackground: '#000000',
35
+ tooltipBorder: '#000000',
36
+ },
37
+ }
38
+
39
+ export function generateChartData(
40
+ count: number,
41
+ series: string[],
42
+ options?: {
43
+ startDate?: Date
44
+ interval?: 'hour' | 'day' | 'week' | 'month'
45
+ trend?: 'up' | 'down' | 'random'
46
+ baseValue?: number
47
+ variance?: number
48
+ }
49
+ ): ChartDataPoint[] {
50
+ const {
51
+ startDate = new Date(),
52
+ interval = 'day',
53
+ trend = 'random',
54
+ baseValue = 100,
55
+ variance = 20,
56
+ } = options || {}
57
+
58
+ const data: ChartDataPoint[] = []
59
+ const current = new Date(startDate)
60
+
61
+ for (let i = 0; i < count; i++) {
62
+ const point: ChartDataPoint = {
63
+ name: formatDateForInterval(current, interval),
64
+ timestamp: current.getTime(),
65
+ }
66
+
67
+ series.forEach((seriesName, index) => {
68
+ let value = baseValue
69
+
70
+ if (trend === 'up') {
71
+ value += (i * 5) + (Math.random() - 0.5) * variance
72
+ } else if (trend === 'down') {
73
+ value -= (i * 5) + (Math.random() - 0.5) * variance
74
+ } else {
75
+ value += (Math.random() - 0.5) * variance * 2
76
+ }
77
+
78
+ // Add some series-specific variation
79
+ value += index * 10 + (Math.random() - 0.5) * 10
80
+
81
+ point[seriesName] = Math.max(0, Math.round(value))
82
+ })
83
+
84
+ data.push(point)
85
+
86
+ // Increment date based on interval
87
+ switch (interval) {
88
+ case 'hour':
89
+ current.setHours(current.getHours() + 1)
90
+ break
91
+ case 'day':
92
+ current.setDate(current.getDate() + 1)
93
+ break
94
+ case 'week':
95
+ current.setDate(current.getDate() + 7)
96
+ break
97
+ case 'month':
98
+ current.setMonth(current.getMonth() + 1)
99
+ break
100
+ }
101
+ }
102
+
103
+ return data
104
+ }
105
+
106
+ function formatDateForInterval(date: Date, interval: 'hour' | 'day' | 'week' | 'month'): string {
107
+ switch (interval) {
108
+ case 'hour':
109
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
110
+ case 'day':
111
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
112
+ case 'week':
113
+ return `Week ${getWeekNumber(date)}`
114
+ case 'month':
115
+ return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
116
+ default:
117
+ return date.toLocaleDateString()
118
+ }
119
+ }
120
+
121
+ function getWeekNumber(date: Date): number {
122
+ const firstDayOfYear = new Date(date.getFullYear(), 0, 1)
123
+ const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000
124
+ return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7)
125
+ }
126
+
127
+ export function calculateMovingAverage(data: ChartDataPoint[], key: string, window: number): ChartDataPoint[] {
128
+ if (window <= 0 || window > data.length) return data
129
+
130
+ return data.map((point, index) => {
131
+ if (index < window - 1) return point
132
+
133
+ const windowData = data.slice(index - window + 1, index + 1)
134
+ const sum = windowData.reduce((acc, item) => acc + (Number(item[key]) || 0), 0)
135
+ const average = sum / window
136
+
137
+ return {
138
+ ...point,
139
+ [`${key}_ma${window}`]: Math.round(average * 100) / 100,
140
+ }
141
+ })
142
+ }
143
+
144
+ export function detectOutliers(data: ChartDataPoint[], key: string, threshold: number = 2): ChartDataPoint[] {
145
+ const values = data.map(point => Number(point[key]) || 0)
146
+ const mean = values.reduce((sum, val) => sum + val, 0) / values.length
147
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length
148
+ const stdDev = Math.sqrt(variance)
149
+
150
+ return data.map(point => {
151
+ const value = Number(point[key]) || 0
152
+ const zScore = Math.abs((value - mean) / stdDev)
153
+
154
+ return {
155
+ ...point,
156
+ [`${key}_outlier`]: zScore > threshold,
157
+ [`${key}_zscore`]: Math.round(zScore * 100) / 100,
158
+ }
159
+ })
160
+ }
161
+
162
+ export function interpolateData(data: ChartDataPoint[], key: string, method: 'linear' | 'polynomial' = 'linear'): ChartDataPoint[] {
163
+ const result = [...data]
164
+
165
+ for (let i = 0; i < result.length; i++) {
166
+ const point = result[i]
167
+
168
+ if (point[key] == null || point[key] === '') {
169
+ // Find nearest non-null values
170
+ let prevIndex = i - 1
171
+ let nextIndex = i + 1
172
+
173
+ while (prevIndex >= 0 && (result[prevIndex][key] == null || result[prevIndex][key] === '')) {
174
+ prevIndex--
175
+ }
176
+
177
+ while (nextIndex < result.length && (result[nextIndex][key] == null || result[nextIndex][key] === '')) {
178
+ nextIndex++
179
+ }
180
+
181
+ if (prevIndex >= 0 && nextIndex < result.length) {
182
+ const prevValue = Number(result[prevIndex][key])
183
+ const nextValue = Number(result[nextIndex][key])
184
+
185
+ if (method === 'linear') {
186
+ const ratio = (i - prevIndex) / (nextIndex - prevIndex)
187
+ point[key] = prevValue + (nextValue - prevValue) * ratio
188
+ }
189
+ } else if (prevIndex >= 0) {
190
+ point[key] = result[prevIndex][key]
191
+ } else if (nextIndex < result.length) {
192
+ point[key] = result[nextIndex][key]
193
+ }
194
+ }
195
+ }
196
+
197
+ return result
198
+ }
199
+
200
+ export function aggregateDataByPeriod(
201
+ data: ChartDataPoint[],
202
+ period: 'hour' | 'day' | 'week' | 'month',
203
+ aggregations: Record<string, 'sum' | 'avg' | 'min' | 'max' | 'count'>
204
+ ): ChartDataPoint[] {
205
+ const groups: Record<string, ChartDataPoint[]> = {}
206
+
207
+ data.forEach(point => {
208
+ const timestamp = point.timestamp ? new Date(point.timestamp as number) : new Date()
209
+ const key = formatDateForInterval(timestamp, period)
210
+
211
+ if (!groups[key]) {
212
+ groups[key] = []
213
+ }
214
+ groups[key].push(point)
215
+ })
216
+
217
+ return Object.entries(groups).map(([key, groupData]) => {
218
+ const result: ChartDataPoint = { name: key }
219
+
220
+ Object.entries(aggregations).forEach(([field, operation]) => {
221
+ const values = groupData.map(point => Number(point[field]) || 0)
222
+
223
+ switch (operation) {
224
+ case 'sum':
225
+ result[field] = values.reduce((sum, val) => sum + val, 0)
226
+ break
227
+ case 'avg':
228
+ result[field] = values.reduce((sum, val) => sum + val, 0) / values.length
229
+ break
230
+ case 'min':
231
+ result[field] = Math.min(...values)
232
+ break
233
+ case 'max':
234
+ result[field] = Math.max(...values)
235
+ break
236
+ case 'count':
237
+ result[field] = values.length
238
+ break
239
+ }
240
+ })
241
+
242
+ return result
243
+ })
244
+ }
245
+
246
+ export function exportChartAsImage(
247
+ chartElement: HTMLElement,
248
+ filename: string = 'chart',
249
+ format: 'png' | 'jpeg' | 'svg' = 'png'
250
+ ): void {
251
+ // This would typically use html2canvas or similar library
252
+ // For now, we'll provide a basic implementation
253
+ console.log(`Exporting chart as ${format} with filename: ${filename}`)
254
+
255
+ // Implementation would go here
256
+ // This is a placeholder for the actual export functionality
257
+ }
@@ -0,0 +1,69 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ // Utility function using clsx and tailwind-merge
5
+ export function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ // Date formatting utility
10
+ export function formatDate(date: Date): string {
11
+ if (isNaN(date.getTime())) {
12
+ return 'Invalid Date'
13
+ }
14
+ return date.toLocaleDateString('en-US', {
15
+ year: 'numeric',
16
+ month: 'short',
17
+ day: 'numeric'
18
+ })
19
+ }
20
+
21
+ // Currency formatting utility
22
+ export function formatCurrency(amount: number): string {
23
+ return new Intl.NumberFormat('en-US', {
24
+ style: 'currency',
25
+ currency: 'USD'
26
+ }).format(amount)
27
+ }
28
+
29
+ // Text truncation utility
30
+ export function truncateText(text: string, maxLength: number): string {
31
+ if (text.length <= maxLength) {
32
+ return text
33
+ }
34
+ return text.slice(0, maxLength) + '...'
35
+ }
36
+
37
+ // ID generation utility
38
+ export function generateId(length: number = 8): string {
39
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
40
+ let result = ''
41
+ for (let i = 0; i < length; i++) {
42
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
43
+ }
44
+ return result
45
+ }
46
+
47
+ // Get client IP address from request
48
+ export function getClientIP(request: Request): string {
49
+ // Check various headers for IP address
50
+ const forwarded = request.headers.get('x-forwarded-for')
51
+ const realIP = request.headers.get('x-real-ip')
52
+ const remoteAddr = request.headers.get('x-remote-addr')
53
+
54
+ if (forwarded) {
55
+ // x-forwarded-for can contain multiple IPs, take the first one
56
+ return forwarded.split(',')[0].trim()
57
+ }
58
+
59
+ if (realIP) {
60
+ return realIP
61
+ }
62
+
63
+ if (remoteAddr) {
64
+ return remoteAddr
65
+ }
66
+
67
+ // Fallback to a default IP if none found
68
+ return '127.0.0.1'
69
+ }