@moontra/moonui-pro 2.20.1 → 2.20.3
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/dist/index.d.ts +691 -261
- package/dist/index.mjs +7418 -4934
- package/package.json +11 -5
- package/plugin/index.d.ts +86 -0
- package/plugin/index.js +308 -0
- package/scripts/postbuild.js +27 -0
- package/scripts/postinstall.js +176 -23
- package/src/__tests__/use-intersection-observer.test.tsx +0 -216
- package/src/__tests__/use-local-storage.test.tsx +0 -174
- package/src/__tests__/use-pro-access.test.tsx +0 -183
- package/src/components/advanced-chart/advanced-chart.test.tsx +0 -281
- package/src/components/advanced-chart/index.tsx +0 -1242
- package/src/components/advanced-forms/index.tsx +0 -426
- package/src/components/animated-button/index.tsx +0 -385
- package/src/components/calendar/event-dialog.tsx +0 -372
- package/src/components/calendar/index.tsx +0 -1073
- package/src/components/calendar-pro/index.tsx +0 -1697
- package/src/components/color-picker/index.tsx +0 -432
- package/src/components/credit-card-input/index.tsx +0 -406
- package/src/components/dashboard/dashboard-grid.tsx +0 -462
- package/src/components/dashboard/demo.tsx +0 -425
- package/src/components/dashboard/index.tsx +0 -1046
- package/src/components/dashboard/time-range-picker.tsx +0 -336
- package/src/components/dashboard/types.ts +0 -222
- package/src/components/dashboard/widgets/activity-feed.tsx +0 -344
- package/src/components/dashboard/widgets/chart-widget.tsx +0 -418
- package/src/components/dashboard/widgets/metric-card.tsx +0 -343
- package/src/components/data-table/data-table-bulk-actions.tsx +0 -204
- package/src/components/data-table/data-table-column-toggle.tsx +0 -169
- package/src/components/data-table/data-table-export.ts +0 -156
- package/src/components/data-table/data-table-filter-drawer.tsx +0 -448
- package/src/components/data-table/data-table.test.tsx +0 -187
- package/src/components/data-table/index.tsx +0 -845
- package/src/components/draggable-list/index.tsx +0 -100
- package/src/components/enhanced/badge.tsx +0 -191
- package/src/components/enhanced/button.tsx +0 -362
- package/src/components/enhanced/card.tsx +0 -266
- package/src/components/enhanced/dialog.tsx +0 -246
- package/src/components/enhanced/index.ts +0 -4
- package/src/components/error-boundary/index.tsx +0 -109
- package/src/components/file-upload/file-upload.test.tsx +0 -243
- package/src/components/file-upload/index.tsx +0 -1660
- package/src/components/floating-action-button/index.tsx +0 -206
- package/src/components/form-wizard/form-wizard-context.tsx +0 -307
- package/src/components/form-wizard/form-wizard-navigation.tsx +0 -118
- package/src/components/form-wizard/form-wizard-progress.tsx +0 -298
- package/src/components/form-wizard/form-wizard-step.tsx +0 -111
- package/src/components/form-wizard/index.tsx +0 -102
- package/src/components/form-wizard/types.ts +0 -76
- package/src/components/gesture-drawer/index.tsx +0 -551
- package/src/components/github-stars/github-api.ts +0 -426
- package/src/components/github-stars/hooks.ts +0 -516
- package/src/components/github-stars/index.tsx +0 -375
- package/src/components/github-stars/types.ts +0 -148
- package/src/components/github-stars/variants.tsx +0 -513
- package/src/components/health-check/index.tsx +0 -439
- package/src/components/hover-card-3d/index.tsx +0 -530
- package/src/components/index.ts +0 -128
- package/src/components/internal/index.ts +0 -78
- package/src/components/kanban/add-card-modal.tsx +0 -502
- package/src/components/kanban/card-detail-modal.tsx +0 -761
- package/src/components/kanban/index.ts +0 -13
- package/src/components/kanban/kanban.tsx +0 -1684
- package/src/components/kanban/types.ts +0 -168
- package/src/components/lazy-component/index.tsx +0 -823
- package/src/components/license-error/index.tsx +0 -29
- package/src/components/magnetic-button/index.tsx +0 -167
- package/src/components/memory-efficient-data/index.tsx +0 -1016
- package/src/components/moonui-quiz-form/index.tsx +0 -817
- package/src/components/optimized-image/index.tsx +0 -425
- package/src/components/performance-debugger/index.tsx +0 -589
- package/src/components/performance-monitor/index.tsx +0 -794
- package/src/components/phone-number-input/index.tsx +0 -338
- package/src/components/pinch-zoom/index.tsx +0 -566
- package/src/components/quiz-form/index.tsx +0 -479
- package/src/components/rich-text-editor/index-old-backup.tsx +0 -437
- package/src/components/rich-text-editor/index.tsx +0 -2324
- package/src/components/rich-text-editor/slash-commands-extension.ts +0 -220
- package/src/components/rich-text-editor/slash-commands.css +0 -35
- package/src/components/rich-text-editor/table-styles.css +0 -65
- package/src/components/sidebar/index.tsx +0 -865
- package/src/components/spotlight-card/index.tsx +0 -191
- package/src/components/swipeable-card/index.tsx +0 -100
- package/src/components/timeline/index.tsx +0 -1148
- package/src/components/ui/accordion.tsx +0 -73
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/alert.tsx +0 -141
- package/src/components/ui/aspect-ratio.tsx +0 -245
- package/src/components/ui/avatar.tsx +0 -153
- package/src/components/ui/badge.tsx +0 -228
- package/src/components/ui/breadcrumb.tsx +0 -214
- package/src/components/ui/button.tsx +0 -222
- package/src/components/ui/calendar.tsx +0 -387
- package/src/components/ui/card.tsx +0 -214
- package/src/components/ui/checkbox.tsx +0 -259
- package/src/components/ui/collapsible.tsx +0 -135
- package/src/components/ui/color-picker.tsx +0 -97
- package/src/components/ui/command.tsx +0 -225
- package/src/components/ui/dialog.tsx +0 -334
- package/src/components/ui/dropdown-menu.tsx +0 -218
- package/src/components/ui/gesture-drawer.tsx +0 -11
- package/src/components/ui/hover-card.tsx +0 -29
- package/src/components/ui/index.ts +0 -190
- package/src/components/ui/input.tsx +0 -222
- package/src/components/ui/label.tsx +0 -29
- package/src/components/ui/lightbox.tsx +0 -606
- package/src/components/ui/magnetic-button.tsx +0 -129
- package/src/components/ui/media-gallery.tsx +0 -612
- package/src/components/ui/pagination.tsx +0 -123
- package/src/components/ui/popover.tsx +0 -185
- package/src/components/ui/progress.tsx +0 -30
- package/src/components/ui/radio-group.tsx +0 -257
- package/src/components/ui/scroll-area.tsx +0 -47
- package/src/components/ui/select.tsx +0 -374
- package/src/components/ui/separator.tsx +0 -145
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/skeleton.tsx +0 -20
- package/src/components/ui/slider.tsx +0 -354
- package/src/components/ui/spotlight-card.tsx +0 -119
- package/src/components/ui/switch.tsx +0 -86
- package/src/components/ui/table.tsx +0 -329
- package/src/components/ui/tabs.tsx +0 -198
- package/src/components/ui/textarea.tsx +0 -28
- package/src/components/ui/toast.tsx +0 -317
- package/src/components/ui/toggle.tsx +0 -119
- package/src/components/ui/tooltip.tsx +0 -151
- package/src/components/virtual-list/index.tsx +0 -668
- package/src/hooks/use-chart.ts +0 -205
- package/src/hooks/use-data-table.ts +0 -182
- package/src/hooks/use-docs-pro-access.ts +0 -13
- package/src/hooks/use-license-check.ts +0 -65
- package/src/hooks/use-subscription.ts +0 -19
- package/src/hooks/use-toast.ts +0 -15
- package/src/index.ts +0 -14
- package/src/lib/ai-providers.ts +0 -377
- package/src/lib/component-metadata.ts +0 -18
- package/src/lib/micro-interactions.ts +0 -255
- package/src/lib/paddle.ts +0 -17
- package/src/lib/utils.ts +0 -6
- package/src/patterns/login-form/index.tsx +0 -276
- package/src/patterns/login-form/types.ts +0 -67
- package/src/setupTests.ts +0 -41
- package/src/styles/advanced-chart.css +0 -239
- package/src/styles/calendar.css +0 -35
- package/src/styles/design-system.css +0 -363
- package/src/styles/index.css +0 -85
- package/src/styles/tailwind.css +0 -7
- package/src/styles/tokens.css +0 -455
- package/src/types/moonui.d.ts +0 -22
- package/src/types/next-auth.d.ts +0 -21
- package/src/use-intersection-observer.tsx +0 -154
- package/src/use-local-storage.tsx +0 -71
- package/src/use-paddle.ts +0 -138
- package/src/use-performance-optimizer.ts +0 -389
- package/src/use-pro-access.ts +0 -141
- package/src/use-scroll-animation.ts +0 -219
- package/src/use-subscription.ts +0 -37
- package/src/use-toast.ts +0 -32
- package/src/utils/chart-helpers.ts +0 -357
- package/src/utils/cn.ts +0 -6
- package/src/utils/data-processing.ts +0 -151
- package/src/utils/license-validator.tsx +0 -183
|
@@ -1,823 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import React, { useState, useEffect, useRef, Suspense, useCallback, useMemo } from "react"
|
|
4
|
-
import { motion, AnimatePresence } from "framer-motion"
|
|
5
|
-
import { Card, CardContent } from "../ui/card"
|
|
6
|
-
import { Button } from "../ui/button"
|
|
7
|
-
import { MoonUISkeletonPro as Skeleton } from "../ui/skeleton"
|
|
8
|
-
import { cn } from "../../lib/utils"
|
|
9
|
-
import { Eye, Loader2, Lock, Sparkles, RefreshCw, Activity, Zap, BarChart3, CheckCircle2, XCircle } from "lucide-react"
|
|
10
|
-
import { useSubscription } from "../../hooks/use-subscription"
|
|
11
|
-
|
|
12
|
-
interface LazyComponentProps {
|
|
13
|
-
children: React.ReactNode
|
|
14
|
-
fallback?: React.ReactNode
|
|
15
|
-
threshold?: number
|
|
16
|
-
rootMargin?: string
|
|
17
|
-
triggerOnce?: boolean
|
|
18
|
-
disabled?: boolean
|
|
19
|
-
onLoad?: () => void
|
|
20
|
-
onVisible?: () => void
|
|
21
|
-
onError?: (error: Error) => void
|
|
22
|
-
showLoadingState?: boolean
|
|
23
|
-
delay?: number
|
|
24
|
-
className?: string
|
|
25
|
-
// Advanced features
|
|
26
|
-
priority?: 'high' | 'normal' | 'low'
|
|
27
|
-
retryCount?: number
|
|
28
|
-
retryDelay?: number
|
|
29
|
-
enableAnalytics?: boolean
|
|
30
|
-
analyticsCallback?: (metrics: LazyLoadMetrics) => void
|
|
31
|
-
prefetch?: boolean
|
|
32
|
-
prefetchDelay?: number
|
|
33
|
-
fadeInDuration?: number
|
|
34
|
-
blurDataURL?: string
|
|
35
|
-
enableProgressiveEnhancement?: boolean
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface LazyLoadMetrics {
|
|
39
|
-
loadTime: number
|
|
40
|
-
visibilityTime: number
|
|
41
|
-
retryAttempts: number
|
|
42
|
-
success: boolean
|
|
43
|
-
componentName?: string
|
|
44
|
-
timestamp: number
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface LoadingState {
|
|
48
|
-
isVisible: boolean
|
|
49
|
-
isLoaded: boolean
|
|
50
|
-
hasError: boolean
|
|
51
|
-
error?: Error
|
|
52
|
-
retryCount: number
|
|
53
|
-
metrics: LazyLoadMetrics
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const LazyComponentInternal: React.FC<LazyComponentProps> = ({
|
|
57
|
-
children,
|
|
58
|
-
fallback,
|
|
59
|
-
threshold = 0.1,
|
|
60
|
-
rootMargin = "50px",
|
|
61
|
-
triggerOnce = true,
|
|
62
|
-
disabled = false,
|
|
63
|
-
onLoad,
|
|
64
|
-
onVisible,
|
|
65
|
-
onError,
|
|
66
|
-
showLoadingState = true,
|
|
67
|
-
delay = 0,
|
|
68
|
-
className,
|
|
69
|
-
priority = 'normal',
|
|
70
|
-
retryCount = 3,
|
|
71
|
-
retryDelay = 1000,
|
|
72
|
-
enableAnalytics = false,
|
|
73
|
-
analyticsCallback,
|
|
74
|
-
prefetch = false,
|
|
75
|
-
prefetchDelay = 2000,
|
|
76
|
-
fadeInDuration = 300,
|
|
77
|
-
blurDataURL,
|
|
78
|
-
enableProgressiveEnhancement = true
|
|
79
|
-
}) => {
|
|
80
|
-
const [loadingState, setLoadingState] = useState<LoadingState>({
|
|
81
|
-
isVisible: disabled,
|
|
82
|
-
isLoaded: disabled,
|
|
83
|
-
hasError: false,
|
|
84
|
-
retryCount: 0,
|
|
85
|
-
metrics: {
|
|
86
|
-
loadTime: 0,
|
|
87
|
-
visibilityTime: 0,
|
|
88
|
-
retryAttempts: 0,
|
|
89
|
-
success: false,
|
|
90
|
-
timestamp: Date.now()
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
const [hasTriggered, setHasTriggered] = useState(false)
|
|
94
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
95
|
-
const visibilityTimeRef = useRef<number>(0)
|
|
96
|
-
const loadStartTimeRef = useRef<number>(0)
|
|
97
|
-
|
|
98
|
-
// Retry logic
|
|
99
|
-
const handleRetry = useCallback(() => {
|
|
100
|
-
setLoadingState(prev => ({
|
|
101
|
-
...prev,
|
|
102
|
-
hasError: false,
|
|
103
|
-
retryCount: prev.retryCount + 1
|
|
104
|
-
}))
|
|
105
|
-
loadStartTimeRef.current = Date.now()
|
|
106
|
-
|
|
107
|
-
// Simulate retry with delay
|
|
108
|
-
setTimeout(() => {
|
|
109
|
-
try {
|
|
110
|
-
setLoadingState(prev => ({
|
|
111
|
-
...prev,
|
|
112
|
-
isLoaded: true,
|
|
113
|
-
hasError: false,
|
|
114
|
-
metrics: {
|
|
115
|
-
...prev.metrics,
|
|
116
|
-
loadTime: Date.now() - loadStartTimeRef.current,
|
|
117
|
-
success: true,
|
|
118
|
-
retryAttempts: prev.retryCount
|
|
119
|
-
}
|
|
120
|
-
}))
|
|
121
|
-
|
|
122
|
-
if (enableAnalytics && analyticsCallback) {
|
|
123
|
-
analyticsCallback(loadingState.metrics)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
onLoad?.()
|
|
127
|
-
} catch (error) {
|
|
128
|
-
if (loadingState.retryCount < retryCount) {
|
|
129
|
-
setTimeout(handleRetry, retryDelay)
|
|
130
|
-
} else {
|
|
131
|
-
setLoadingState(prev => ({
|
|
132
|
-
...prev,
|
|
133
|
-
hasError: true,
|
|
134
|
-
error: error as Error
|
|
135
|
-
}))
|
|
136
|
-
onError?.(error as Error)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}, delay)
|
|
140
|
-
}, [delay, retryCount, retryDelay, onLoad, onError, enableAnalytics, analyticsCallback, loadingState.metrics, loadingState.retryCount])
|
|
141
|
-
|
|
142
|
-
// Prefetch logic
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
if (prefetch && !disabled) {
|
|
145
|
-
const prefetchTimer = setTimeout(() => {
|
|
146
|
-
// Prefetch logic here
|
|
147
|
-
console.log('Prefetching component...')
|
|
148
|
-
}, prefetchDelay)
|
|
149
|
-
|
|
150
|
-
return () => clearTimeout(prefetchTimer)
|
|
151
|
-
}
|
|
152
|
-
}, [prefetch, prefetchDelay, disabled])
|
|
153
|
-
|
|
154
|
-
useEffect(() => {
|
|
155
|
-
if (disabled) {
|
|
156
|
-
setLoadingState(prev => ({
|
|
157
|
-
...prev,
|
|
158
|
-
isVisible: true,
|
|
159
|
-
isLoaded: true
|
|
160
|
-
}))
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const observer = new IntersectionObserver(
|
|
165
|
-
([entry]) => {
|
|
166
|
-
if (entry.isIntersecting && (!triggerOnce || !hasTriggered)) {
|
|
167
|
-
visibilityTimeRef.current = Date.now()
|
|
168
|
-
|
|
169
|
-
if (delay > 0) {
|
|
170
|
-
setTimeout(() => {
|
|
171
|
-
setLoadingState(prev => ({ ...prev, isVisible: true }))
|
|
172
|
-
setHasTriggered(true)
|
|
173
|
-
onVisible?.()
|
|
174
|
-
|
|
175
|
-
loadStartTimeRef.current = Date.now()
|
|
176
|
-
|
|
177
|
-
// Simulate loading with priority
|
|
178
|
-
const loadDelay = priority === 'high' ? delay * 0.5 : priority === 'low' ? delay * 1.5 : delay
|
|
179
|
-
|
|
180
|
-
setTimeout(() => {
|
|
181
|
-
try {
|
|
182
|
-
setLoadingState(prev => ({
|
|
183
|
-
...prev,
|
|
184
|
-
isLoaded: true,
|
|
185
|
-
metrics: {
|
|
186
|
-
...prev.metrics,
|
|
187
|
-
loadTime: Date.now() - loadStartTimeRef.current,
|
|
188
|
-
visibilityTime: Date.now() - visibilityTimeRef.current,
|
|
189
|
-
success: true
|
|
190
|
-
}
|
|
191
|
-
}))
|
|
192
|
-
|
|
193
|
-
if (enableAnalytics && analyticsCallback) {
|
|
194
|
-
analyticsCallback(loadingState.metrics)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
onLoad?.()
|
|
198
|
-
} catch (error) {
|
|
199
|
-
handleRetry()
|
|
200
|
-
}
|
|
201
|
-
}, loadDelay)
|
|
202
|
-
}, 100)
|
|
203
|
-
} else {
|
|
204
|
-
setLoadingState(prev => ({
|
|
205
|
-
...prev,
|
|
206
|
-
isVisible: true,
|
|
207
|
-
isLoaded: true
|
|
208
|
-
}))
|
|
209
|
-
setHasTriggered(true)
|
|
210
|
-
onVisible?.()
|
|
211
|
-
onLoad?.()
|
|
212
|
-
}
|
|
213
|
-
} else if (!entry.isIntersecting && !triggerOnce) {
|
|
214
|
-
setLoadingState(prev => ({
|
|
215
|
-
...prev,
|
|
216
|
-
isVisible: false,
|
|
217
|
-
isLoaded: false
|
|
218
|
-
}))
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
threshold,
|
|
223
|
-
rootMargin
|
|
224
|
-
}
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
if (containerRef.current) {
|
|
228
|
-
observer.observe(containerRef.current)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return () => {
|
|
232
|
-
if (containerRef.current) {
|
|
233
|
-
observer.unobserve(containerRef.current)
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}, [threshold, rootMargin, triggerOnce, delay, disabled, hasTriggered, onLoad, onVisible, priority, enableAnalytics, analyticsCallback, handleRetry])
|
|
237
|
-
|
|
238
|
-
const renderFallback = () => {
|
|
239
|
-
if (loadingState.hasError) {
|
|
240
|
-
return (
|
|
241
|
-
<motion.div
|
|
242
|
-
initial={{ opacity: 0 }}
|
|
243
|
-
animate={{ opacity: 1 }}
|
|
244
|
-
className="flex items-center justify-center p-8 border border-destructive/50 rounded-lg bg-destructive/10"
|
|
245
|
-
>
|
|
246
|
-
<div className="text-center space-y-3">
|
|
247
|
-
<XCircle className="h-8 w-8 mx-auto text-destructive" />
|
|
248
|
-
<div>
|
|
249
|
-
<p className="text-sm font-medium">Failed to load component</p>
|
|
250
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
251
|
-
{loadingState.error?.message || 'An error occurred'}
|
|
252
|
-
</p>
|
|
253
|
-
</div>
|
|
254
|
-
{loadingState.retryCount < retryCount && (
|
|
255
|
-
<Button
|
|
256
|
-
size="sm"
|
|
257
|
-
variant="outline"
|
|
258
|
-
onClick={handleRetry}
|
|
259
|
-
className="mt-2"
|
|
260
|
-
>
|
|
261
|
-
<RefreshCw className="h-3 w-3 mr-2" />
|
|
262
|
-
Retry ({loadingState.retryCount}/{retryCount})
|
|
263
|
-
</Button>
|
|
264
|
-
)}
|
|
265
|
-
</div>
|
|
266
|
-
</motion.div>
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (fallback) {
|
|
271
|
-
return fallback
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (showLoadingState) {
|
|
275
|
-
return (
|
|
276
|
-
<motion.div
|
|
277
|
-
initial={{ opacity: 0 }}
|
|
278
|
-
animate={{ opacity: 1 }}
|
|
279
|
-
className="relative"
|
|
280
|
-
>
|
|
281
|
-
{blurDataURL && enableProgressiveEnhancement ? (
|
|
282
|
-
<div className="relative overflow-hidden rounded-lg">
|
|
283
|
-
<img
|
|
284
|
-
src={blurDataURL}
|
|
285
|
-
alt="Loading placeholder"
|
|
286
|
-
className="w-full h-32 object-cover filter blur-xl"
|
|
287
|
-
/>
|
|
288
|
-
<div className="absolute inset-0 bg-background/50 backdrop-blur-sm" />
|
|
289
|
-
</div>
|
|
290
|
-
) : (
|
|
291
|
-
<div className="flex items-center justify-center p-8 bg-muted/50 rounded-lg">
|
|
292
|
-
<div className="text-center space-y-3">
|
|
293
|
-
{delay > 0 && loadingState.isVisible && !loadingState.isLoaded ? (
|
|
294
|
-
<>
|
|
295
|
-
<div className="relative">
|
|
296
|
-
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
|
|
297
|
-
{priority === 'high' && (
|
|
298
|
-
<Zap className="h-3 w-3 absolute -top-1 -right-1 text-yellow-500" />
|
|
299
|
-
)}
|
|
300
|
-
</div>
|
|
301
|
-
<div className="space-y-1">
|
|
302
|
-
<p className="text-sm font-medium">Loading component...</p>
|
|
303
|
-
<p className="text-xs text-muted-foreground">
|
|
304
|
-
Priority: {priority} • Attempt {loadingState.retryCount + 1}
|
|
305
|
-
</p>
|
|
306
|
-
</div>
|
|
307
|
-
</>
|
|
308
|
-
) : (
|
|
309
|
-
<>
|
|
310
|
-
<Eye className="h-8 w-8 mx-auto text-muted-foreground" />
|
|
311
|
-
<p className="text-sm text-muted-foreground">
|
|
312
|
-
Scroll to load content
|
|
313
|
-
</p>
|
|
314
|
-
</>
|
|
315
|
-
)}
|
|
316
|
-
</div>
|
|
317
|
-
</div>
|
|
318
|
-
)}
|
|
319
|
-
|
|
320
|
-
{enableAnalytics && loadingState.isVisible && !loadingState.isLoaded && (
|
|
321
|
-
<div className="absolute top-2 right-2 flex items-center gap-2 text-xs text-muted-foreground bg-background/80 backdrop-blur-sm px-2 py-1 rounded">
|
|
322
|
-
<Activity className="h-3 w-3" />
|
|
323
|
-
<span>Tracking performance</span>
|
|
324
|
-
</div>
|
|
325
|
-
)}
|
|
326
|
-
</motion.div>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return <Skeleton className="w-full h-32" />
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return (
|
|
334
|
-
<div ref={containerRef} className={cn("w-full", className)}>
|
|
335
|
-
<AnimatePresence mode="wait">
|
|
336
|
-
{loadingState.isLoaded ? (
|
|
337
|
-
<motion.div
|
|
338
|
-
key="content"
|
|
339
|
-
initial={{ opacity: 0, y: 20 }}
|
|
340
|
-
animate={{ opacity: 1, y: 0 }}
|
|
341
|
-
exit={{ opacity: 0, y: -20 }}
|
|
342
|
-
transition={{ duration: fadeInDuration / 1000 }}
|
|
343
|
-
>
|
|
344
|
-
{enableAnalytics && (
|
|
345
|
-
<div className="absolute -top-8 right-0 flex items-center gap-2 text-xs text-muted-foreground bg-background/80 backdrop-blur-sm px-2 py-1 rounded">
|
|
346
|
-
<CheckCircle2 className="h-3 w-3 text-green-500" />
|
|
347
|
-
<span>Loaded in {loadingState.metrics.loadTime}ms</span>
|
|
348
|
-
</div>
|
|
349
|
-
)}
|
|
350
|
-
{children}
|
|
351
|
-
</motion.div>
|
|
352
|
-
) : (
|
|
353
|
-
<motion.div
|
|
354
|
-
key="fallback"
|
|
355
|
-
initial={{ opacity: 0 }}
|
|
356
|
-
animate={{ opacity: 1 }}
|
|
357
|
-
exit={{ opacity: 0 }}
|
|
358
|
-
transition={{ duration: 0.2 }}
|
|
359
|
-
>
|
|
360
|
-
{renderFallback()}
|
|
361
|
-
</motion.div>
|
|
362
|
-
)}
|
|
363
|
-
</AnimatePresence>
|
|
364
|
-
</div>
|
|
365
|
-
)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Lazy Image Component
|
|
369
|
-
export interface LazyImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'onProgress'> {
|
|
370
|
-
src: string
|
|
371
|
-
alt: string
|
|
372
|
-
fallbackSrc?: string
|
|
373
|
-
showPlaceholder?: boolean
|
|
374
|
-
threshold?: number
|
|
375
|
-
rootMargin?: string
|
|
376
|
-
onLoad?: () => void
|
|
377
|
-
onError?: () => void
|
|
378
|
-
className?: string
|
|
379
|
-
// Advanced features
|
|
380
|
-
quality?: number
|
|
381
|
-
priority?: boolean
|
|
382
|
-
blur?: boolean
|
|
383
|
-
blurDataURL?: string
|
|
384
|
-
aspectRatio?: string
|
|
385
|
-
objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
|
|
386
|
-
sizes?: string
|
|
387
|
-
srcSet?: string
|
|
388
|
-
enableProgressiveLoading?: boolean
|
|
389
|
-
onProgress?: (progress: number) => void
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export const LazyImage: React.FC<LazyImageProps> = ({
|
|
393
|
-
src,
|
|
394
|
-
alt,
|
|
395
|
-
fallbackSrc,
|
|
396
|
-
showPlaceholder = true,
|
|
397
|
-
threshold = 0.1,
|
|
398
|
-
rootMargin = "50px",
|
|
399
|
-
onLoad,
|
|
400
|
-
onError,
|
|
401
|
-
className,
|
|
402
|
-
quality = 85,
|
|
403
|
-
priority = false,
|
|
404
|
-
blur = true,
|
|
405
|
-
blurDataURL,
|
|
406
|
-
aspectRatio,
|
|
407
|
-
objectFit = 'cover',
|
|
408
|
-
sizes,
|
|
409
|
-
srcSet,
|
|
410
|
-
enableProgressiveLoading = false,
|
|
411
|
-
onProgress,
|
|
412
|
-
...restProps
|
|
413
|
-
}) => {
|
|
414
|
-
// Filter out LazyImage specific props and event handlers that shouldn't be passed to img element
|
|
415
|
-
const { onDrag, onDragStart, onDragEnd, onDragOver, onDrop, onAnimationStart, onAnimationEnd, onAnimationIteration, ...props } = restProps
|
|
416
|
-
const [imageSrc, setImageSrc] = useState<string | null>(priority ? src : null)
|
|
417
|
-
const [imageError, setImageError] = useState(false)
|
|
418
|
-
const [isVisible, setIsVisible] = useState(priority)
|
|
419
|
-
const [loadProgress, setLoadProgress] = useState(0)
|
|
420
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
421
|
-
const imgRef = useRef<HTMLImageElement>(null)
|
|
422
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
423
|
-
|
|
424
|
-
useEffect(() => {
|
|
425
|
-
const observer = new IntersectionObserver(
|
|
426
|
-
([entry]) => {
|
|
427
|
-
if (entry.isIntersecting) {
|
|
428
|
-
setIsVisible(true)
|
|
429
|
-
observer.disconnect()
|
|
430
|
-
}
|
|
431
|
-
},
|
|
432
|
-
{ threshold, rootMargin }
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
if (containerRef.current) {
|
|
436
|
-
observer.observe(containerRef.current)
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return () => observer.disconnect()
|
|
440
|
-
}, [threshold, rootMargin])
|
|
441
|
-
|
|
442
|
-
useEffect(() => {
|
|
443
|
-
if (!isVisible || imageSrc) return
|
|
444
|
-
|
|
445
|
-
setIsLoading(true)
|
|
446
|
-
const img = new Image()
|
|
447
|
-
|
|
448
|
-
// Progressive loading simulation
|
|
449
|
-
if (enableProgressiveLoading) {
|
|
450
|
-
let progress = 0
|
|
451
|
-
const progressInterval = setInterval(() => {
|
|
452
|
-
progress += Math.random() * 30
|
|
453
|
-
if (progress > 90) progress = 90
|
|
454
|
-
setLoadProgress(progress)
|
|
455
|
-
onProgress?.(progress)
|
|
456
|
-
}, 100)
|
|
457
|
-
|
|
458
|
-
img.onload = () => {
|
|
459
|
-
clearInterval(progressInterval)
|
|
460
|
-
setLoadProgress(100)
|
|
461
|
-
onProgress?.(100)
|
|
462
|
-
setImageSrc(src)
|
|
463
|
-
setIsLoading(false)
|
|
464
|
-
onLoad?.()
|
|
465
|
-
}
|
|
466
|
-
} else {
|
|
467
|
-
img.onload = () => {
|
|
468
|
-
setImageSrc(src)
|
|
469
|
-
setIsLoading(false)
|
|
470
|
-
onLoad?.()
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
img.onerror = () => {
|
|
475
|
-
setImageError(true)
|
|
476
|
-
setIsLoading(false)
|
|
477
|
-
if (fallbackSrc) {
|
|
478
|
-
setImageSrc(fallbackSrc)
|
|
479
|
-
}
|
|
480
|
-
onError?.()
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Add quality parameter to image URL if supported
|
|
484
|
-
const qualityParam = src.includes('?') ? `&q=${quality}` : `?q=${quality}`
|
|
485
|
-
img.src = src + (src.includes('http') ? qualityParam : '')
|
|
486
|
-
}, [isVisible, src, fallbackSrc, onLoad, onError, quality, enableProgressiveLoading, onProgress, imageSrc])
|
|
487
|
-
|
|
488
|
-
return (
|
|
489
|
-
<div
|
|
490
|
-
ref={containerRef}
|
|
491
|
-
className={cn("relative overflow-hidden bg-muted", className)}
|
|
492
|
-
style={aspectRatio ? { aspectRatio } : undefined}
|
|
493
|
-
>
|
|
494
|
-
<AnimatePresence mode="wait">
|
|
495
|
-
{/* Blur placeholder */}
|
|
496
|
-
{blur && blurDataURL && !imageSrc && (
|
|
497
|
-
<motion.img
|
|
498
|
-
key="blur"
|
|
499
|
-
src={blurDataURL}
|
|
500
|
-
alt=""
|
|
501
|
-
initial={{ opacity: 0 }}
|
|
502
|
-
animate={{ opacity: 1 }}
|
|
503
|
-
exit={{ opacity: 0 }}
|
|
504
|
-
className="absolute inset-0 w-full h-full object-cover filter blur-xl scale-110"
|
|
505
|
-
/>
|
|
506
|
-
)}
|
|
507
|
-
|
|
508
|
-
{/* Main image */}
|
|
509
|
-
{imageSrc && (
|
|
510
|
-
<motion.img
|
|
511
|
-
key="main"
|
|
512
|
-
ref={imgRef}
|
|
513
|
-
src={imageSrc}
|
|
514
|
-
alt={alt}
|
|
515
|
-
sizes={sizes}
|
|
516
|
-
srcSet={srcSet}
|
|
517
|
-
initial={{ opacity: 0, scale: 1.02 }}
|
|
518
|
-
animate={{ opacity: 1, scale: 1 }}
|
|
519
|
-
transition={{ duration: 0.4 }}
|
|
520
|
-
className={cn("w-full h-full", `object-${objectFit}`)}
|
|
521
|
-
{...props}
|
|
522
|
-
/>
|
|
523
|
-
)}
|
|
524
|
-
|
|
525
|
-
{/* Loading state */}
|
|
526
|
-
{isLoading && showPlaceholder && (
|
|
527
|
-
<motion.div
|
|
528
|
-
key="loading"
|
|
529
|
-
initial={{ opacity: 0 }}
|
|
530
|
-
animate={{ opacity: 1 }}
|
|
531
|
-
exit={{ opacity: 0 }}
|
|
532
|
-
className="absolute inset-0 flex items-center justify-center bg-background/50 backdrop-blur-sm"
|
|
533
|
-
>
|
|
534
|
-
<div className="text-center">
|
|
535
|
-
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary mb-2" />
|
|
536
|
-
{enableProgressiveLoading && (
|
|
537
|
-
<div className="w-32 h-1 bg-muted rounded-full overflow-hidden">
|
|
538
|
-
<motion.div
|
|
539
|
-
className="h-full bg-primary"
|
|
540
|
-
initial={{ width: 0 }}
|
|
541
|
-
animate={{ width: `${loadProgress}%` }}
|
|
542
|
-
/>
|
|
543
|
-
</div>
|
|
544
|
-
)}
|
|
545
|
-
</div>
|
|
546
|
-
</motion.div>
|
|
547
|
-
)}
|
|
548
|
-
|
|
549
|
-
{/* Skeleton placeholder */}
|
|
550
|
-
{!imageSrc && !isLoading && showPlaceholder && (
|
|
551
|
-
<Skeleton className="absolute inset-0" />
|
|
552
|
-
)}
|
|
553
|
-
|
|
554
|
-
{/* Error state */}
|
|
555
|
-
{imageError && !fallbackSrc && (
|
|
556
|
-
<motion.div
|
|
557
|
-
initial={{ opacity: 0 }}
|
|
558
|
-
animate={{ opacity: 1 }}
|
|
559
|
-
className="absolute inset-0 flex items-center justify-center bg-muted"
|
|
560
|
-
>
|
|
561
|
-
<div className="text-center">
|
|
562
|
-
<XCircle className="h-8 w-8 mx-auto text-muted-foreground mb-2" />
|
|
563
|
-
<p className="text-sm text-muted-foreground">Failed to load image</p>
|
|
564
|
-
</div>
|
|
565
|
-
</motion.div>
|
|
566
|
-
)}
|
|
567
|
-
</AnimatePresence>
|
|
568
|
-
|
|
569
|
-
{/* Priority badge */}
|
|
570
|
-
{priority && (
|
|
571
|
-
<div className="absolute top-2 right-2 bg-yellow-500/90 text-yellow-900 text-xs px-2 py-1 rounded flex items-center gap-1">
|
|
572
|
-
<Zap className="h-3 w-3" />
|
|
573
|
-
Priority
|
|
574
|
-
</div>
|
|
575
|
-
)}
|
|
576
|
-
</div>
|
|
577
|
-
)
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Lazy List Component
|
|
581
|
-
export interface LazyListProps<T> {
|
|
582
|
-
items: T[]
|
|
583
|
-
renderItem: (item: T, index: number) => React.ReactNode
|
|
584
|
-
itemHeight?: number
|
|
585
|
-
batchSize?: number
|
|
586
|
-
threshold?: number
|
|
587
|
-
className?: string
|
|
588
|
-
// Advanced features
|
|
589
|
-
enableVirtualization?: boolean
|
|
590
|
-
overscan?: number
|
|
591
|
-
showLoadingIndicator?: boolean
|
|
592
|
-
loadingIndicator?: React.ReactNode
|
|
593
|
-
emptyState?: React.ReactNode
|
|
594
|
-
onBatchLoad?: (startIndex: number, endIndex: number) => void
|
|
595
|
-
enableSmoothScroll?: boolean
|
|
596
|
-
staggerAnimation?: boolean
|
|
597
|
-
animationDelay?: number
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export function LazyList<T>({
|
|
601
|
-
items,
|
|
602
|
-
renderItem,
|
|
603
|
-
itemHeight = 100,
|
|
604
|
-
batchSize = 10,
|
|
605
|
-
threshold = 0.5,
|
|
606
|
-
className,
|
|
607
|
-
enableVirtualization = false,
|
|
608
|
-
overscan = 3,
|
|
609
|
-
showLoadingIndicator = true,
|
|
610
|
-
loadingIndicator,
|
|
611
|
-
emptyState,
|
|
612
|
-
onBatchLoad,
|
|
613
|
-
enableSmoothScroll = true,
|
|
614
|
-
staggerAnimation = true,
|
|
615
|
-
animationDelay = 50
|
|
616
|
-
}: LazyListProps<T>) {
|
|
617
|
-
const [visibleItems, setVisibleItems] = useState<T[]>([])
|
|
618
|
-
const [currentBatch, setCurrentBatch] = useState(0)
|
|
619
|
-
const [isLoading, setIsLoading] = useState(false)
|
|
620
|
-
const loadingRef = useRef<HTMLDivElement>(null)
|
|
621
|
-
const containerRef = useRef<HTMLDivElement>(null)
|
|
622
|
-
const [scrollPosition, setScrollPosition] = useState(0)
|
|
623
|
-
|
|
624
|
-
useEffect(() => {
|
|
625
|
-
setVisibleItems(items.slice(0, batchSize))
|
|
626
|
-
setCurrentBatch(1)
|
|
627
|
-
}, [items, batchSize])
|
|
628
|
-
|
|
629
|
-
// Handle scroll for virtualization
|
|
630
|
-
const handleScroll = useCallback((e: Event) => {
|
|
631
|
-
if (enableVirtualization && containerRef.current) {
|
|
632
|
-
setScrollPosition((e.target as HTMLElement).scrollTop)
|
|
633
|
-
}
|
|
634
|
-
}, [enableVirtualization])
|
|
635
|
-
|
|
636
|
-
useEffect(() => {
|
|
637
|
-
const container = containerRef.current
|
|
638
|
-
if (enableVirtualization && container) {
|
|
639
|
-
container.addEventListener('scroll', handleScroll)
|
|
640
|
-
return () => container.removeEventListener('scroll', handleScroll)
|
|
641
|
-
}
|
|
642
|
-
}, [enableVirtualization, handleScroll])
|
|
643
|
-
|
|
644
|
-
useEffect(() => {
|
|
645
|
-
const observer = new IntersectionObserver(
|
|
646
|
-
([entry]) => {
|
|
647
|
-
if (entry.isIntersecting && currentBatch * batchSize < items.length && !isLoading) {
|
|
648
|
-
setIsLoading(true)
|
|
649
|
-
|
|
650
|
-
// Simulate loading delay
|
|
651
|
-
setTimeout(() => {
|
|
652
|
-
const nextBatch = currentBatch + 1
|
|
653
|
-
const startIndex = currentBatch * batchSize
|
|
654
|
-
const endIndex = nextBatch * batchSize
|
|
655
|
-
const newItems = items.slice(0, endIndex)
|
|
656
|
-
|
|
657
|
-
setVisibleItems(newItems)
|
|
658
|
-
setCurrentBatch(nextBatch)
|
|
659
|
-
setIsLoading(false)
|
|
660
|
-
|
|
661
|
-
onBatchLoad?.(startIndex, endIndex)
|
|
662
|
-
}, 300)
|
|
663
|
-
}
|
|
664
|
-
},
|
|
665
|
-
{ threshold }
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
if (loadingRef.current) {
|
|
669
|
-
observer.observe(loadingRef.current)
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return () => observer.disconnect()
|
|
673
|
-
}, [currentBatch, items, batchSize, threshold, isLoading, onBatchLoad])
|
|
674
|
-
|
|
675
|
-
// Calculate visible range for virtualization
|
|
676
|
-
const visibleRange = useMemo(() => {
|
|
677
|
-
if (!enableVirtualization) return { start: 0, end: visibleItems.length }
|
|
678
|
-
|
|
679
|
-
const start = Math.floor(scrollPosition / itemHeight) - overscan
|
|
680
|
-
const visibleCount = Math.ceil(400 / itemHeight) // Assuming container height of 400px
|
|
681
|
-
const end = start + visibleCount + overscan * 2
|
|
682
|
-
|
|
683
|
-
return {
|
|
684
|
-
start: Math.max(0, start),
|
|
685
|
-
end: Math.min(visibleItems.length, end)
|
|
686
|
-
}
|
|
687
|
-
}, [scrollPosition, itemHeight, overscan, visibleItems.length, enableVirtualization])
|
|
688
|
-
|
|
689
|
-
if (items.length === 0 && emptyState) {
|
|
690
|
-
return (
|
|
691
|
-
<div className={cn("flex items-center justify-center p-8", className)}>
|
|
692
|
-
{emptyState}
|
|
693
|
-
</div>
|
|
694
|
-
)
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
return (
|
|
698
|
-
<div
|
|
699
|
-
ref={containerRef}
|
|
700
|
-
className={cn(
|
|
701
|
-
"relative",
|
|
702
|
-
enableVirtualization && "overflow-auto max-h-[400px]",
|
|
703
|
-
enableSmoothScroll && "scroll-smooth",
|
|
704
|
-
className
|
|
705
|
-
)}
|
|
706
|
-
>
|
|
707
|
-
{enableVirtualization ? (
|
|
708
|
-
// Virtualized rendering
|
|
709
|
-
<>
|
|
710
|
-
<div style={{ height: visibleItems.length * itemHeight }} />
|
|
711
|
-
<div className="absolute top-0 left-0 right-0">
|
|
712
|
-
{visibleItems.slice(visibleRange.start, visibleRange.end).map((item, index) => {
|
|
713
|
-
const actualIndex = visibleRange.start + index
|
|
714
|
-
return (
|
|
715
|
-
<motion.div
|
|
716
|
-
key={actualIndex}
|
|
717
|
-
initial={staggerAnimation ? { opacity: 0, x: -20 } : { opacity: 1 }}
|
|
718
|
-
animate={{ opacity: 1, x: 0 }}
|
|
719
|
-
transition={staggerAnimation ? {
|
|
720
|
-
delay: index * (animationDelay / 1000),
|
|
721
|
-
duration: 0.3
|
|
722
|
-
} : undefined}
|
|
723
|
-
style={{
|
|
724
|
-
position: 'absolute',
|
|
725
|
-
top: actualIndex * itemHeight,
|
|
726
|
-
left: 0,
|
|
727
|
-
right: 0,
|
|
728
|
-
height: itemHeight
|
|
729
|
-
}}
|
|
730
|
-
>
|
|
731
|
-
{renderItem(item, actualIndex)}
|
|
732
|
-
</motion.div>
|
|
733
|
-
)
|
|
734
|
-
})}
|
|
735
|
-
</div>
|
|
736
|
-
</>
|
|
737
|
-
) : (
|
|
738
|
-
// Regular lazy loading
|
|
739
|
-
<div className="space-y-2">
|
|
740
|
-
{visibleItems.map((item, index) => (
|
|
741
|
-
<motion.div
|
|
742
|
-
key={index}
|
|
743
|
-
initial={staggerAnimation ? { opacity: 0, y: 20 } : { opacity: 1 }}
|
|
744
|
-
animate={{ opacity: 1, y: 0 }}
|
|
745
|
-
transition={staggerAnimation ? {
|
|
746
|
-
delay: (index % batchSize) * (animationDelay / 1000),
|
|
747
|
-
duration: 0.3
|
|
748
|
-
} : undefined}
|
|
749
|
-
style={{ minHeight: itemHeight }}
|
|
750
|
-
>
|
|
751
|
-
{renderItem(item, index)}
|
|
752
|
-
</motion.div>
|
|
753
|
-
))}
|
|
754
|
-
</div>
|
|
755
|
-
)}
|
|
756
|
-
|
|
757
|
-
{currentBatch * batchSize < items.length && (
|
|
758
|
-
<div ref={loadingRef} className="flex justify-center py-4">
|
|
759
|
-
{showLoadingIndicator && (
|
|
760
|
-
loadingIndicator || (
|
|
761
|
-
<div className="flex items-center gap-2">
|
|
762
|
-
<Loader2 className="h-5 w-5 animate-spin text-primary" />
|
|
763
|
-
<span className="text-sm text-muted-foreground">
|
|
764
|
-
Loading more items...
|
|
765
|
-
</span>
|
|
766
|
-
</div>
|
|
767
|
-
)
|
|
768
|
-
)}
|
|
769
|
-
</div>
|
|
770
|
-
)}
|
|
771
|
-
|
|
772
|
-
{/* Progress indicator */}
|
|
773
|
-
{items.length > batchSize && (
|
|
774
|
-
<div className="sticky bottom-0 left-0 right-0 bg-gradient-to-t from-background to-transparent pt-8 pb-2">
|
|
775
|
-
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
|
776
|
-
<BarChart3 className="h-3 w-3" />
|
|
777
|
-
<span>
|
|
778
|
-
Showing {visibleItems.length} of {items.length} items
|
|
779
|
-
</span>
|
|
780
|
-
</div>
|
|
781
|
-
</div>
|
|
782
|
-
)}
|
|
783
|
-
</div>
|
|
784
|
-
)
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
export const LazyComponent: React.FC<LazyComponentProps> = ({ className, ...props }) => {
|
|
788
|
-
// Check if we're in docs mode or have pro access
|
|
789
|
-
const { hasProAccess, isLoading } = useSubscription()
|
|
790
|
-
|
|
791
|
-
// In docs mode, always show the component
|
|
792
|
-
|
|
793
|
-
// If not in docs mode and no pro access, show upgrade prompt
|
|
794
|
-
if (!isLoading && !hasProAccess) {
|
|
795
|
-
return (
|
|
796
|
-
<Card className={cn("w-fit", className)}>
|
|
797
|
-
<CardContent className="py-6 text-center">
|
|
798
|
-
<div className="space-y-4">
|
|
799
|
-
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
800
|
-
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
801
|
-
</div>
|
|
802
|
-
<div>
|
|
803
|
-
<h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
|
|
804
|
-
<p className="text-muted-foreground text-xs mb-4">
|
|
805
|
-
Lazy Component is available exclusively to MoonUI Pro subscribers.
|
|
806
|
-
</p>
|
|
807
|
-
<a href="/pricing">
|
|
808
|
-
<Button size="sm">
|
|
809
|
-
<Sparkles className="mr-2 h-4 w-4" />
|
|
810
|
-
Upgrade to Pro
|
|
811
|
-
</Button>
|
|
812
|
-
</a>
|
|
813
|
-
</div>
|
|
814
|
-
</div>
|
|
815
|
-
</CardContent>
|
|
816
|
-
</Card>
|
|
817
|
-
)
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
return <LazyComponentInternal className={className} {...props} />
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Export types removed to fix conflict
|