@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,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
|
+
}
|
package/src/use-toast.ts
ADDED
|
@@ -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
|
+
}
|
package/src/utils/cn.ts
ADDED
|
@@ -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
|
+
}
|