@moontra/moonui-pro 2.0.22 → 2.1.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.
Files changed (99) hide show
  1. package/dist/index.mjs +215 -214
  2. package/package.json +4 -2
  3. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  4. package/src/__tests__/use-local-storage.test.tsx +174 -0
  5. package/src/__tests__/use-pro-access.test.tsx +183 -0
  6. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  7. package/src/components/advanced-chart/index.tsx +412 -0
  8. package/src/components/advanced-forms/index.tsx +431 -0
  9. package/src/components/animated-button/index.tsx +202 -0
  10. package/src/components/calendar/event-dialog.tsx +372 -0
  11. package/src/components/calendar/index.tsx +557 -0
  12. package/src/components/color-picker/index.tsx +434 -0
  13. package/src/components/dashboard/index.tsx +334 -0
  14. package/src/components/data-table/data-table.test.tsx +187 -0
  15. package/src/components/data-table/index.tsx +368 -0
  16. package/src/components/draggable-list/index.tsx +100 -0
  17. package/src/components/enhanced/button.tsx +360 -0
  18. package/src/components/enhanced/card.tsx +272 -0
  19. package/src/components/enhanced/dialog.tsx +248 -0
  20. package/src/components/enhanced/index.ts +3 -0
  21. package/src/components/error-boundary/index.tsx +111 -0
  22. package/src/components/file-upload/file-upload.test.tsx +242 -0
  23. package/src/components/file-upload/index.tsx +362 -0
  24. package/src/components/floating-action-button/index.tsx +209 -0
  25. package/src/components/github-stars/index.tsx +414 -0
  26. package/src/components/health-check/index.tsx +441 -0
  27. package/src/components/hover-card-3d/index.tsx +170 -0
  28. package/src/components/index.ts +76 -0
  29. package/src/components/kanban/index.tsx +436 -0
  30. package/src/components/lazy-component/index.tsx +342 -0
  31. package/src/components/magnetic-button/index.tsx +170 -0
  32. package/src/components/memory-efficient-data/index.tsx +352 -0
  33. package/src/components/optimized-image/index.tsx +427 -0
  34. package/src/components/performance-debugger/index.tsx +591 -0
  35. package/src/components/performance-monitor/index.tsx +775 -0
  36. package/src/components/pinch-zoom/index.tsx +172 -0
  37. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  38. package/src/components/rich-text-editor/index.tsx +1537 -0
  39. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  40. package/src/components/rich-text-editor/slash-commands.css +35 -0
  41. package/src/components/rich-text-editor/table-styles.css +65 -0
  42. package/src/components/spotlight-card/index.tsx +194 -0
  43. package/src/components/swipeable-card/index.tsx +100 -0
  44. package/src/components/timeline/index.tsx +333 -0
  45. package/src/components/ui/animated-button.tsx +185 -0
  46. package/src/components/ui/avatar.tsx +135 -0
  47. package/src/components/ui/badge.tsx +225 -0
  48. package/src/components/ui/button.tsx +221 -0
  49. package/src/components/ui/card.tsx +141 -0
  50. package/src/components/ui/checkbox.tsx +256 -0
  51. package/src/components/ui/color-picker.tsx +95 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/hover-card-3d.tsx +103 -0
  55. package/src/components/ui/index.ts +33 -0
  56. package/src/components/ui/input.tsx +219 -0
  57. package/src/components/ui/label.tsx +26 -0
  58. package/src/components/ui/magnetic-button.tsx +129 -0
  59. package/src/components/ui/popover.tsx +183 -0
  60. package/src/components/ui/select.tsx +273 -0
  61. package/src/components/ui/separator.tsx +140 -0
  62. package/src/components/ui/slider.tsx +351 -0
  63. package/src/components/ui/spotlight-card.tsx +119 -0
  64. package/src/components/ui/switch.tsx +83 -0
  65. package/src/components/ui/tabs.tsx +195 -0
  66. package/src/components/ui/textarea.tsx +25 -0
  67. package/src/components/ui/toast.tsx +313 -0
  68. package/src/components/ui/tooltip.tsx +152 -0
  69. package/src/components/virtual-list/index.tsx +369 -0
  70. package/src/hooks/use-chart.ts +205 -0
  71. package/src/hooks/use-data-table.ts +182 -0
  72. package/src/hooks/use-docs-pro-access.ts +13 -0
  73. package/src/hooks/use-license-check.ts +65 -0
  74. package/src/hooks/use-subscription.ts +19 -0
  75. package/src/index.ts +14 -0
  76. package/src/lib/micro-interactions.ts +255 -0
  77. package/src/lib/utils.ts +6 -0
  78. package/src/patterns/login-form/index.tsx +276 -0
  79. package/src/patterns/login-form/types.ts +67 -0
  80. package/src/setupTests.ts +41 -0
  81. package/src/styles/design-system.css +365 -0
  82. package/src/styles/index.css +4 -0
  83. package/src/styles/tailwind.css +6 -0
  84. package/src/styles/tokens.css +453 -0
  85. package/src/types/moonui.d.ts +22 -0
  86. package/src/use-intersection-observer.tsx +154 -0
  87. package/src/use-local-storage.tsx +71 -0
  88. package/src/use-paddle.ts +138 -0
  89. package/src/use-performance-optimizer.ts +379 -0
  90. package/src/use-pro-access.ts +141 -0
  91. package/src/use-scroll-animation.ts +221 -0
  92. package/src/use-subscription.ts +37 -0
  93. package/src/use-toast.ts +32 -0
  94. package/src/utils/chart-helpers.ts +257 -0
  95. package/src/utils/cn.ts +69 -0
  96. package/src/utils/data-processing.ts +151 -0
  97. package/src/utils/license-guard.tsx +177 -0
  98. package/src/utils/license-validator.tsx +183 -0
  99. package/src/utils/package-guard.ts +60 -0
@@ -0,0 +1,138 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { getPaddleInstance } from '@/lib/paddle';
3
+ import type { Paddle } from '@paddle/paddle-js';
4
+
5
+ interface UsePaddleReturn {
6
+ paddle: Paddle | null;
7
+ isLoading: boolean;
8
+ error: string | null;
9
+ openCheckout: (planId: string) => Promise<void>;
10
+ }
11
+
12
+ /**
13
+ * Custom hook for Paddle integration
14
+ * Handles Paddle initialization and checkout operations
15
+ */
16
+ export function usePaddle(): UsePaddleReturn {
17
+ const [paddle, setPaddle] = useState<Paddle | null>(null);
18
+ const [isLoading, setIsLoading] = useState(true);
19
+ const [error, setError] = useState<string | null>(null);
20
+
21
+ // Initialize Paddle on mount
22
+ useEffect(() => {
23
+ let mounted = true;
24
+
25
+ const initPaddle = async () => {
26
+ try {
27
+ setIsLoading(true);
28
+ setError(null);
29
+
30
+ const paddleInstance = await getPaddleInstance();
31
+
32
+ if (mounted) {
33
+ if (paddleInstance) {
34
+ setPaddle(paddleInstance);
35
+ } else {
36
+ setError('Failed to initialize Paddle');
37
+ }
38
+ }
39
+ } catch (err) {
40
+ if (mounted) {
41
+ setError(err instanceof Error ? err.message : 'Unknown error');
42
+ }
43
+ } finally {
44
+ if (mounted) {
45
+ setIsLoading(false);
46
+ }
47
+ }
48
+ };
49
+
50
+ initPaddle();
51
+
52
+ return () => {
53
+ mounted = false;
54
+ };
55
+ }, []);
56
+
57
+ /**
58
+ * Open Paddle checkout for a specific plan
59
+ */
60
+ const openCheckout = async (planId: string) => {
61
+ if (!paddle) {
62
+ throw new Error('Paddle not initialized');
63
+ }
64
+
65
+ try {
66
+ // Call our API to handle the checkout
67
+ const response = await fetch('/api/paddle', {
68
+ method: 'POST',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ },
72
+ body: JSON.stringify({ plan: planId }),
73
+ });
74
+
75
+ if (!response.ok) {
76
+ const errorText = await response.text();
77
+ throw new Error(errorText || 'Failed to create checkout');
78
+ }
79
+
80
+ const result = await response.json();
81
+
82
+ if (!result.success) {
83
+ throw new Error(result.message || 'Checkout failed');
84
+ }
85
+
86
+ // For mock mode, we need to manually trigger checkout
87
+ if (process.env.NEXT_PUBLIC_USE_MOCK_PADDLE === 'true') {
88
+ console.log('🧪 [Hook] Mock mode detected, triggering checkout');
89
+
90
+ // Import paddle config to get price ID
91
+ const { PADDLE_CONFIG } = await import('@/lib/paddle');
92
+ const priceId = PADDLE_CONFIG.priceIds[planId as keyof typeof PADDLE_CONFIG.priceIds];
93
+
94
+ console.log('🧪 [Hook] Using price ID:', priceId, 'for plan:', planId);
95
+ console.log('🧪 [Hook] Paddle instance:', paddle);
96
+
97
+ // Trigger mock checkout
98
+ const checkoutData = {
99
+ items: [{ priceId, quantity: 1 }],
100
+ settings: {
101
+ displayMode: 'overlay' as const,
102
+ theme: 'light' as const,
103
+ locale: 'en' as const,
104
+ successUrl: `${window.location.origin}/dashboard?success=true`
105
+ },
106
+ customData: {
107
+ planId,
108
+ userId: 'test_user',
109
+ timestamp: new Date().toISOString()
110
+ }
111
+ };
112
+
113
+ console.log('🧪 [Hook] Opening checkout with data:', checkoutData);
114
+
115
+ try {
116
+ paddle.Checkout.open(checkoutData);
117
+ console.log('🧪 [Hook] Checkout.open called successfully');
118
+ } catch (checkoutError) {
119
+ console.error('🧪 [Hook] Checkout.open error:', checkoutError);
120
+ }
121
+ }
122
+
123
+ // Checkout is opened inline by Paddle
124
+ console.log('✅ Paddle checkout opened for plan:', planId);
125
+
126
+ } catch (error) {
127
+ console.error('❌ Checkout error:', error);
128
+ throw error;
129
+ }
130
+ };
131
+
132
+ return {
133
+ paddle,
134
+ isLoading,
135
+ error,
136
+ openCheckout,
137
+ };
138
+ }
@@ -0,0 +1,379 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { performanceProfiler } from "@/lib/performance-profiler";
5
+
6
+ // Performance optimization hooks
7
+ export function usePerformanceOptimizer() {
8
+ const [optimizations, setOptimizations] = React.useState<OptimizationSuggestion[]>([]);
9
+ const [isAnalyzing, setIsAnalyzing] = React.useState(false);
10
+
11
+ const analyzePerformance = React.useCallback(async () => {
12
+ setIsAnalyzing(true);
13
+
14
+ try {
15
+ const metrics = performanceProfiler.getPerformanceSummary();
16
+ const suggestions: OptimizationSuggestion[] = [];
17
+
18
+ // Analyze component render times
19
+ const componentMetrics = performanceProfiler.getMeasurements('component');
20
+ if (componentMetrics.length > 0) {
21
+ const slowComponents = componentMetrics
22
+ .filter(m => m.duration > 16.67) // Slower than 60fps
23
+ .sort((a, b) => b.duration - a.duration)
24
+ .slice(0, 5);
25
+
26
+ slowComponents.forEach(component => {
27
+ suggestions.push({
28
+ type: 'component',
29
+ severity: component.duration > 50 ? 'high' : 'medium',
30
+ title: `Slow component: ${component.name}`,
31
+ description: `Component takes ${component.duration.toFixed(2)}ms to render`,
32
+ solution: 'Consider using React.memo, useMemo, or useCallback',
33
+ impact: 'high',
34
+ });
35
+ });
36
+ }
37
+
38
+ // Analyze memory usage
39
+ if ('memory' in performance) {
40
+ const memory = (performance as any).memory;
41
+ const usagePercent = (memory.usedJSHeapSize / memory.totalJSHeapSize) * 100;
42
+
43
+ if (usagePercent > 80) {
44
+ suggestions.push({
45
+ type: 'memory',
46
+ severity: 'high',
47
+ title: 'High memory usage',
48
+ description: `Memory usage is at ${usagePercent.toFixed(1)}%`,
49
+ solution: 'Review for memory leaks, optimize data structures, use lazy loading',
50
+ impact: 'high',
51
+ });
52
+ }
53
+ }
54
+
55
+ // Analyze layout shifts
56
+ const layoutShifts = performanceProfiler.getMeasurements('layout-shift');
57
+ if (layoutShifts.length > 0) {
58
+ const totalShift = layoutShifts.reduce((sum, shift) => sum + (shift.value || 0), 0);
59
+
60
+ if (totalShift > 0.1) {
61
+ suggestions.push({
62
+ type: 'layout',
63
+ severity: 'medium',
64
+ title: 'Layout shifts detected',
65
+ description: `Cumulative Layout Shift: ${totalShift.toFixed(3)}`,
66
+ solution: 'Add dimensions to images, reserve space for dynamic content',
67
+ impact: 'medium',
68
+ });
69
+ }
70
+ }
71
+
72
+ // Analyze bundle size (if available)
73
+ if (typeof window !== 'undefined' && window.performance) {
74
+ const resources = performance.getEntriesByType('resource') as PerformanceResourceTiming[];
75
+ const jsResources = resources.filter(r => r.name.includes('.js'));
76
+ const largeResources = jsResources.filter(r => r.transferSize > 100000); // >100KB
77
+
78
+ if (largeResources.length > 0) {
79
+ suggestions.push({
80
+ type: 'bundle',
81
+ severity: 'medium',
82
+ title: 'Large JavaScript bundles',
83
+ description: `${largeResources.length} JavaScript files exceed 100KB`,
84
+ solution: 'Implement code splitting, lazy loading, and tree shaking',
85
+ impact: 'medium',
86
+ });
87
+ }
88
+ }
89
+
90
+ setOptimizations(suggestions);
91
+ } catch (error) {
92
+ console.error('Performance analysis failed:', error);
93
+ } finally {
94
+ setIsAnalyzing(false);
95
+ }
96
+ }, []);
97
+
98
+ return {
99
+ optimizations,
100
+ isAnalyzing,
101
+ analyzePerformance,
102
+ };
103
+ }
104
+
105
+ // Hook for render optimization
106
+ export function useRenderOptimization<T>(
107
+ value: T,
108
+ dependencies: React.DependencyList
109
+ ): T {
110
+ const memoizedValue = React.useMemo(() => value, dependencies);
111
+
112
+ React.useEffect(() => {
113
+ if (process.env.NODE_ENV === 'development') {
114
+ const componentName = 'useRenderOptimization';
115
+ performanceProfiler.measureComponent(componentName, () => {
116
+ // Measure the impact of memoization
117
+ });
118
+ }
119
+ }, dependencies);
120
+
121
+ return memoizedValue;
122
+ }
123
+
124
+ // Hook for debounced performance tracking
125
+ export function useDebouncePerformance<T extends (...args: any[]) => void>(
126
+ callback: T,
127
+ delay: number = 300
128
+ ): T {
129
+ const timeoutRef = React.useRef<NodeJS.Timeout>();
130
+
131
+ const debouncedCallback = React.useCallback(
132
+ (...args: Parameters<T>) => {
133
+ if (timeoutRef.current) {
134
+ clearTimeout(timeoutRef.current);
135
+ }
136
+
137
+ timeoutRef.current = setTimeout(() => {
138
+ performanceProfiler.measureComponent('debounced-callback', () => {
139
+ callback(...args);
140
+ });
141
+ }, delay);
142
+ },
143
+ [callback, delay]
144
+ ) as T;
145
+
146
+ React.useEffect(() => {
147
+ return () => {
148
+ if (timeoutRef.current) {
149
+ clearTimeout(timeoutRef.current);
150
+ }
151
+ };
152
+ }, []);
153
+
154
+ return debouncedCallback;
155
+ }
156
+
157
+ // Hook for throttled performance tracking
158
+ export function useThrottlePerformance<T extends (...args: any[]) => void>(
159
+ callback: T,
160
+ delay: number = 100
161
+ ): T {
162
+ const lastCallRef = React.useRef<number>(0);
163
+
164
+ const throttledCallback = React.useCallback(
165
+ (...args: Parameters<T>) => {
166
+ const now = Date.now();
167
+
168
+ if (now - lastCallRef.current >= delay) {
169
+ lastCallRef.current = now;
170
+ performanceProfiler.measureComponent('throttled-callback', () => {
171
+ callback(...args);
172
+ });
173
+ }
174
+ },
175
+ [callback, delay]
176
+ ) as T;
177
+
178
+ return throttledCallback;
179
+ }
180
+
181
+ // Hook for measuring component lifecycle
182
+ export function useComponentLifecycleTracking(componentName: string) {
183
+ const mountTime = React.useRef<number>(0);
184
+ const updateCount = React.useRef<number>(0);
185
+
186
+ React.useEffect(() => {
187
+ // Component mount
188
+ mountTime.current = performance.now();
189
+
190
+ performanceProfiler.measureComponent(`${componentName}-mount`, () => {
191
+ // Measure mount time
192
+ });
193
+
194
+ return () => {
195
+ // Component unmount
196
+ const lifetime = performance.now() - mountTime.current;
197
+ performanceProfiler.measureComponent(`${componentName}-unmount`, () => {
198
+ // Record component lifetime
199
+ });
200
+ };
201
+ }, [componentName]);
202
+
203
+ React.useEffect(() => {
204
+ // Component update
205
+ updateCount.current++;
206
+
207
+ if (updateCount.current > 1) {
208
+ performanceProfiler.measureComponent(`${componentName}-update`, () => {
209
+ // Measure update time
210
+ });
211
+ }
212
+ });
213
+
214
+ return {
215
+ mountTime: mountTime.current,
216
+ updateCount: updateCount.current,
217
+ };
218
+ }
219
+
220
+ // Hook for virtual scrolling optimization
221
+ export function useVirtualScrolling<T>(
222
+ items: T[],
223
+ itemHeight: number,
224
+ containerHeight: number,
225
+ overscan: number = 3
226
+ ) {
227
+ const [scrollTop, setScrollTop] = React.useState(0);
228
+
229
+ const visibleRange = React.useMemo(() => {
230
+ const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
231
+ const endIndex = Math.min(
232
+ items.length - 1,
233
+ Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
234
+ );
235
+
236
+ return { startIndex, endIndex };
237
+ }, [scrollTop, itemHeight, containerHeight, overscan, items.length]);
238
+
239
+ const visibleItems = React.useMemo(() => {
240
+ return items.slice(visibleRange.startIndex, visibleRange.endIndex + 1);
241
+ }, [items, visibleRange]);
242
+
243
+ const totalHeight = items.length * itemHeight;
244
+ const offsetY = visibleRange.startIndex * itemHeight;
245
+
246
+ return {
247
+ visibleItems,
248
+ totalHeight,
249
+ offsetY,
250
+ startIndex: visibleRange.startIndex,
251
+ endIndex: visibleRange.endIndex,
252
+ setScrollTop,
253
+ };
254
+ }
255
+
256
+ // Hook for image lazy loading optimization
257
+ export function useLazyImageOptimization(src: string, threshold: number = 0.1) {
258
+ const [isLoaded, setIsLoaded] = React.useState(false);
259
+ const [isIntersecting, setIsIntersecting] = React.useState(false);
260
+ const imgRef = React.useRef<HTMLImageElement>(null);
261
+
262
+ React.useEffect(() => {
263
+ const observer = new IntersectionObserver(
264
+ ([entry]) => {
265
+ if (entry.isIntersecting) {
266
+ setIsIntersecting(true);
267
+
268
+ // Measure image load time
269
+ const startTime = performance.now();
270
+ const img = new Image();
271
+
272
+ img.onload = () => {
273
+ const loadTime = performance.now() - startTime;
274
+ performanceProfiler.measureComponent('image-load', () => {
275
+ // Record image load time
276
+ });
277
+ setIsLoaded(true);
278
+ };
279
+
280
+ img.src = src;
281
+ observer.disconnect();
282
+ }
283
+ },
284
+ { threshold }
285
+ );
286
+
287
+ if (imgRef.current) {
288
+ observer.observe(imgRef.current);
289
+ }
290
+
291
+ return () => observer.disconnect();
292
+ }, [src, threshold]);
293
+
294
+ return {
295
+ imgRef,
296
+ isLoaded,
297
+ isIntersecting,
298
+ shouldLoad: isIntersecting,
299
+ };
300
+ }
301
+
302
+ // Performance optimization interfaces
303
+ interface OptimizationSuggestion {
304
+ type: 'component' | 'memory' | 'layout' | 'bundle' | 'network';
305
+ severity: 'low' | 'medium' | 'high';
306
+ title: string;
307
+ description: string;
308
+ solution: string;
309
+ impact: 'low' | 'medium' | 'high';
310
+ }
311
+
312
+ // Hook for automatic performance optimization
313
+ export function useAutoPerformanceOptimization() {
314
+ const [isOptimizing, setIsOptimizing] = React.useState(false);
315
+ const [optimizations, setOptimizations] = React.useState<string[]>([]);
316
+
317
+ const applyOptimizations = React.useCallback(async () => {
318
+ setIsOptimizing(true);
319
+ const applied: string[] = [];
320
+
321
+ try {
322
+ // Apply automatic optimizations
323
+
324
+ // 1. Prefetch critical resources
325
+ const criticalResources = document.querySelectorAll('link[rel="preload"]');
326
+ if (criticalResources.length === 0) {
327
+ // Add preload hints for critical resources
328
+ const cssLinks = document.querySelectorAll('link[rel="stylesheet"]');
329
+ cssLinks.forEach(link => {
330
+ const preloadLink = document.createElement('link');
331
+ preloadLink.rel = 'preload';
332
+ preloadLink.href = (link as HTMLLinkElement).href;
333
+ preloadLink.as = 'style';
334
+ document.head.appendChild(preloadLink);
335
+ });
336
+ applied.push('Added preload hints for CSS');
337
+ }
338
+
339
+ // 2. Optimize images
340
+ const images = document.querySelectorAll('img:not([loading])');
341
+ images.forEach(img => {
342
+ (img as HTMLImageElement).loading = 'lazy';
343
+ });
344
+ if (images.length > 0) {
345
+ applied.push(`Applied lazy loading to ${images.length} images`);
346
+ }
347
+
348
+ // 3. Add intersection observers for below-fold content
349
+ const belowFoldElements = document.querySelectorAll('[data-optimize="lazy"]');
350
+ belowFoldElements.forEach(element => {
351
+ const observer = new IntersectionObserver(
352
+ ([entry]) => {
353
+ if (entry.isIntersecting) {
354
+ element.classList.add('optimized');
355
+ observer.disconnect();
356
+ }
357
+ },
358
+ { threshold: 0.1 }
359
+ );
360
+ observer.observe(element);
361
+ });
362
+ if (belowFoldElements.length > 0) {
363
+ applied.push(`Added intersection observers to ${belowFoldElements.length} elements`);
364
+ }
365
+
366
+ setOptimizations(applied);
367
+ } catch (error) {
368
+ console.error('Auto-optimization failed:', error);
369
+ } finally {
370
+ setIsOptimizing(false);
371
+ }
372
+ }, []);
373
+
374
+ return {
375
+ isOptimizing,
376
+ optimizations,
377
+ applyOptimizations,
378
+ };
379
+ }
@@ -0,0 +1,141 @@
1
+ // Pro Access Hook
2
+ // Kullanıcının pro componentlere erişim durumunu kontrol eder
3
+
4
+ import React from 'react'
5
+ import { useSession } from 'next-auth/react'
6
+ import { useQuery } from '@tanstack/react-query'
7
+ import { getComponentAccess } from '@/lib/component-metadata'
8
+
9
+ export interface UserSubscription {
10
+ userId: string
11
+ plan: 'free' | 'pro_monthly' | 'pro_annual' | 'pro_lifetime'
12
+ status: 'active' | 'expired' | 'cancelled' | 'trial'
13
+ expiresAt: Date | null // lifetime için null
14
+ createdAt: Date
15
+ updatedAt: Date
16
+ }
17
+
18
+ export interface ProAccessStatus {
19
+ hasProAccess: boolean
20
+ subscription: UserSubscription | null | undefined
21
+ isLoading: boolean
22
+ error: Error | null
23
+ daysUntilExpiry: number | null
24
+ }
25
+
26
+ // Subscription durumunu API'den çek
27
+ async function fetchSubscriptionStatus(): Promise<UserSubscription | null> {
28
+ const response = await fetch('/api/user/subscription-status')
29
+
30
+ if (!response.ok) {
31
+ throw new Error('Subscription status fetch failed')
32
+ }
33
+
34
+ const data = await response.json()
35
+ return data.subscription
36
+ }
37
+
38
+ // Pro erişim durumunu hesapla
39
+ function calculateProAccess(subscription: UserSubscription | null | undefined): {
40
+ hasProAccess: boolean
41
+ daysUntilExpiry: number | null
42
+ } {
43
+ if (!subscription) {
44
+ return { hasProAccess: false, daysUntilExpiry: null }
45
+ }
46
+
47
+ // Lifetime üyelik
48
+ if (subscription.plan === 'pro_lifetime') {
49
+ return { hasProAccess: true, daysUntilExpiry: null }
50
+ }
51
+
52
+ // Aktif olmayan subscription
53
+ if (subscription.status !== 'active') {
54
+ return { hasProAccess: false, daysUntilExpiry: null }
55
+ }
56
+
57
+ // Süre kontrolü
58
+ if (subscription.expiresAt) {
59
+ const now = new Date()
60
+ const expiryDate = new Date(subscription.expiresAt)
61
+ const daysUntilExpiry = Math.ceil((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
62
+
63
+ return {
64
+ hasProAccess: daysUntilExpiry > 0,
65
+ daysUntilExpiry: daysUntilExpiry > 0 ? daysUntilExpiry : 0
66
+ }
67
+ }
68
+
69
+ return { hasProAccess: false, daysUntilExpiry: null }
70
+ }
71
+
72
+ export function useProAccess(): ProAccessStatus {
73
+ const { data: session, status } = useSession()
74
+
75
+ const {
76
+ data: subscription,
77
+ isLoading,
78
+ error
79
+ } = useQuery({
80
+ queryKey: ['subscription-status', session?.user?.id],
81
+ queryFn: fetchSubscriptionStatus,
82
+ enabled: !!session?.user?.id && status === 'authenticated',
83
+ staleTime: 5 * 60 * 1000, // 5 dakika cache
84
+ refetchInterval: 10 * 60 * 1000, // 10 dakikada bir otomatik refresh
85
+ })
86
+
87
+ const { hasProAccess, daysUntilExpiry } = calculateProAccess(subscription)
88
+
89
+ return {
90
+ hasProAccess,
91
+ subscription,
92
+ isLoading: status === 'loading' || isLoading,
93
+ error: error as Error | null,
94
+ daysUntilExpiry
95
+ }
96
+ }
97
+
98
+ // Component erişim kontrolü için helper hook
99
+ export function useComponentAccess(componentId: string) {
100
+ const { hasProAccess, isLoading } = useProAccess()
101
+
102
+ const access = React.useMemo(() => {
103
+ return getComponentAccess(componentId, hasProAccess ? 'pro' : 'free')
104
+ }, [componentId, hasProAccess])
105
+
106
+ return {
107
+ ...access,
108
+ isLoading,
109
+ canUse: access.access === 'unlocked',
110
+ isLocked: access.access === 'locked'
111
+ }
112
+ }
113
+
114
+ // Subscription durumu için utility functions
115
+ export function getSubscriptionDisplayName(plan: string): string {
116
+ switch (plan) {
117
+ case 'pro_monthly':
118
+ return 'Pro Monthly'
119
+ case 'pro_annual':
120
+ return 'Pro Annual'
121
+ case 'pro_lifetime':
122
+ return 'Pro Lifetime'
123
+ default:
124
+ return 'Free'
125
+ }
126
+ }
127
+
128
+ export function getSubscriptionColor(status: string): string {
129
+ switch (status) {
130
+ case 'active':
131
+ return 'green'
132
+ case 'trial':
133
+ return 'blue'
134
+ case 'expired':
135
+ return 'red'
136
+ case 'cancelled':
137
+ return 'gray'
138
+ default:
139
+ return 'gray'
140
+ }
141
+ }