@djangocfg/ui-nextjs 1.4.45
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/LICENSE +21 -0
- package/README.md +152 -0
- package/package.json +110 -0
- package/src/animations/AnimatedBackground.tsx +645 -0
- package/src/animations/index.ts +2 -0
- package/src/blocks/ArticleCard.tsx +94 -0
- package/src/blocks/ArticleList.tsx +95 -0
- package/src/blocks/CTASection.tsx +136 -0
- package/src/blocks/FeatureSection.tsx +104 -0
- package/src/blocks/Hero.tsx +102 -0
- package/src/blocks/NewsletterSection.tsx +119 -0
- package/src/blocks/StatsSection.tsx +103 -0
- package/src/blocks/SuperHero.tsx +328 -0
- package/src/blocks/TestimonialSection.tsx +122 -0
- package/src/blocks/index.ts +9 -0
- package/src/components/README.md +2018 -0
- package/src/components/breadcrumb-navigation.tsx +127 -0
- package/src/components/breadcrumb.tsx +132 -0
- package/src/components/button-download.tsx +275 -0
- package/src/components/dropdown-menu.tsx +219 -0
- package/src/components/index.ts +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +338 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/components/menubar.tsx +274 -0
- package/src/components/multi-select-pro/async.tsx +608 -0
- package/src/components/multi-select-pro/helpers.tsx +84 -0
- package/src/components/multi-select-pro/index.tsx +622 -0
- package/src/components/navigation-menu.tsx +153 -0
- package/src/components/pagination-static.tsx +348 -0
- package/src/components/pagination.tsx +138 -0
- package/src/components/phone-input.tsx +276 -0
- package/src/components/sidebar.tsx +866 -0
- package/src/components/sonner.tsx +31 -0
- package/src/components/ssr-pagination.tsx +237 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/useCfgRouter.ts +153 -0
- package/src/hooks/useLocalStorage.ts +221 -0
- package/src/hooks/useQueryParams.ts +73 -0
- package/src/hooks/useSessionStorage.ts +188 -0
- package/src/hooks/useTheme.ts +57 -0
- package/src/index.ts +24 -0
- package/src/lib/index.ts +2 -0
- package/src/styles/index.css +2 -0
- package/src/theme/ForceTheme.tsx +115 -0
- package/src/theme/ThemeProvider.tsx +82 -0
- package/src/theme/ThemeToggle.tsx +52 -0
- package/src/theme/index.ts +3 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +212 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
- package/src/tools/JsonForm/widgets/index.ts +12 -0
- package/src/tools/JsonTree/index.tsx +252 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
- package/src/tools/LottiePlayer/index.tsx +54 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +163 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
- package/src/tools/Mermaid/index.tsx +40 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
- package/src/tools/OpenapiViewer/index.tsx +36 -0
- package/src/tools/OpenapiViewer/types.ts +152 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
- package/src/tools/PrettyCode/index.tsx +43 -0
- package/src/tools/VideoPlayer/README.md +239 -0
- package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
- package/src/tools/VideoPlayer/index.ts +9 -0
- package/src/tools/VideoPlayer/types.ts +62 -0
- package/src/tools/index.ts +43 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from '@djangocfg/ui-core/components';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
interface Stat {
|
|
7
|
+
number: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StatsSectionProps {
|
|
14
|
+
title: string;
|
|
15
|
+
subtitle?: string;
|
|
16
|
+
stats: Stat[];
|
|
17
|
+
columns?: 2 | 3 | 4;
|
|
18
|
+
className?: string;
|
|
19
|
+
background?: 'dark' | 'card' | 'gradient';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const StatsSection: React.FC<StatsSectionProps> = ({
|
|
23
|
+
title,
|
|
24
|
+
subtitle,
|
|
25
|
+
stats,
|
|
26
|
+
columns = 4,
|
|
27
|
+
className,
|
|
28
|
+
background = 'dark'
|
|
29
|
+
}) => {
|
|
30
|
+
const getGridClasses = () => {
|
|
31
|
+
switch (columns) {
|
|
32
|
+
case 2:
|
|
33
|
+
return 'grid-cols-1 sm:grid-cols-2';
|
|
34
|
+
case 3:
|
|
35
|
+
return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3';
|
|
36
|
+
case 4:
|
|
37
|
+
return 'grid-cols-2 sm:grid-cols-2 lg:grid-cols-4';
|
|
38
|
+
default:
|
|
39
|
+
return 'grid-cols-2 sm:grid-cols-2 lg:grid-cols-4';
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getBackgroundClasses = () => {
|
|
44
|
+
switch (background) {
|
|
45
|
+
case 'dark':
|
|
46
|
+
return 'bg-background';
|
|
47
|
+
case 'card':
|
|
48
|
+
return 'gradient-card';
|
|
49
|
+
case 'gradient':
|
|
50
|
+
return 'gradient-subtle';
|
|
51
|
+
default:
|
|
52
|
+
return 'bg-background';
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<section className={cn('py-12 sm:py-16 lg:py-24', getBackgroundClasses(), className)}>
|
|
58
|
+
<div className="w-full px-4 sm:px-6 lg:px-8">
|
|
59
|
+
<div className="text-center mb-12 sm:mb-16 animate-fade-in">
|
|
60
|
+
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-foreground mb-4 sm:mb-6">
|
|
61
|
+
{title}
|
|
62
|
+
</h2>
|
|
63
|
+
{subtitle && (
|
|
64
|
+
<p className="text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-2">
|
|
65
|
+
{subtitle}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className={cn('grid gap-6 sm:gap-8', getGridClasses())}>
|
|
71
|
+
{stats.map((stat, index) => (
|
|
72
|
+
<Card
|
|
73
|
+
key={index}
|
|
74
|
+
className="text-center hover:shadow-large transition-all duration-300 animate-scale-in backdrop-blur-sm border-border/50 hover:border-primary/30 group"
|
|
75
|
+
style={{ animationDelay: `${index * 0.1}s` }}
|
|
76
|
+
>
|
|
77
|
+
<CardContent className="p-6 sm:p-8">
|
|
78
|
+
{stat.icon && (
|
|
79
|
+
<div className="w-12 h-12 sm:w-16 sm:h-16 bg-primary/10 text-primary rounded-lg flex items-center justify-center mx-auto mb-4 sm:mb-6 group-hover:scale-110 group-hover:bg-primary/20 transition-all duration-300">
|
|
80
|
+
<div className="text-xl sm:text-2xl">
|
|
81
|
+
{stat.icon}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
<div className="text-3xl sm:text-4xl lg:text-5xl font-bold text-foreground mb-2 sm:mb-3 group-hover:text-primary transition-colors duration-300">
|
|
86
|
+
{stat.number}
|
|
87
|
+
</div>
|
|
88
|
+
<div className="text-sm sm:text-base text-muted-foreground font-medium">
|
|
89
|
+
{stat.label}
|
|
90
|
+
</div>
|
|
91
|
+
{stat.description && (
|
|
92
|
+
<p className="text-xs sm:text-sm text-muted-foreground mt-2">
|
|
93
|
+
{stat.description}
|
|
94
|
+
</p>
|
|
95
|
+
)}
|
|
96
|
+
</CardContent>
|
|
97
|
+
</Card>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ArrowRight, Sparkles, Wand2, Copy, Check } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
7
|
+
import { Button, ButtonLink, Sticky, Tooltip, TooltipTrigger, TooltipContent } from '@djangocfg/ui-core/components';
|
|
8
|
+
import { useCopy } from '@djangocfg/ui-core/hooks';
|
|
9
|
+
import { AnimatedBackground, type BackgroundVariant } from '../animations';
|
|
10
|
+
import { ForceTheme } from '../theme';
|
|
11
|
+
|
|
12
|
+
interface HeroFeature {
|
|
13
|
+
icon: React.ReactNode;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface StatItem {
|
|
18
|
+
number: string;
|
|
19
|
+
label: string;
|
|
20
|
+
icon?: React.ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SuperHeroProps {
|
|
24
|
+
badge?: {
|
|
25
|
+
icon?: React.ReactNode;
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
title: string;
|
|
29
|
+
titleGradient?: string;
|
|
30
|
+
subtitle: string;
|
|
31
|
+
features?: HeroFeature[];
|
|
32
|
+
primaryAction?: {
|
|
33
|
+
label: string;
|
|
34
|
+
href?: string;
|
|
35
|
+
onClick?: () => void;
|
|
36
|
+
};
|
|
37
|
+
secondaryAction?: {
|
|
38
|
+
label: string;
|
|
39
|
+
href?: string;
|
|
40
|
+
onClick?: () => void;
|
|
41
|
+
icon?: React.ReactNode;
|
|
42
|
+
};
|
|
43
|
+
stats?: StatItem[];
|
|
44
|
+
floatingElements?: React.ReactNode;
|
|
45
|
+
backgroundVariant?: BackgroundVariant;
|
|
46
|
+
backgroundIntensity?: 'subtle' | 'medium' | 'strong';
|
|
47
|
+
showBackgroundSwitcher?: boolean;
|
|
48
|
+
codeCommand?: string;
|
|
49
|
+
className?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const BACKGROUND_VARIANTS: BackgroundVariant[] = [
|
|
53
|
+
'aurora-borealis',
|
|
54
|
+
'mesh-gradient',
|
|
55
|
+
'floating-orbs',
|
|
56
|
+
'geometric-flow',
|
|
57
|
+
'liquid-gradient',
|
|
58
|
+
'spotlight',
|
|
59
|
+
'none'
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const VARIANT_LABELS: Partial<Record<BackgroundVariant, string>> = {
|
|
63
|
+
'aurora-borealis': 'Aurora Borealis',
|
|
64
|
+
'mesh-gradient': 'Mesh Gradient',
|
|
65
|
+
'floating-orbs': 'Floating Orbs',
|
|
66
|
+
'geometric-flow': 'Geometric Flow',
|
|
67
|
+
'liquid-gradient': 'Liquid Gradient',
|
|
68
|
+
'spotlight': 'Spotlight',
|
|
69
|
+
'none': 'None',
|
|
70
|
+
// Legacy labels for backwards compatibility
|
|
71
|
+
'gradient-mesh': 'Gradient Mesh',
|
|
72
|
+
'dot-matrix': 'Dot Matrix',
|
|
73
|
+
'grid-lines': 'Grid Lines',
|
|
74
|
+
'aurora': 'Aurora',
|
|
75
|
+
'particles': 'Particles',
|
|
76
|
+
'waves': 'Waves',
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const SuperHero: React.FC<SuperHeroProps> = ({
|
|
80
|
+
badge,
|
|
81
|
+
title,
|
|
82
|
+
titleGradient,
|
|
83
|
+
subtitle,
|
|
84
|
+
features = [],
|
|
85
|
+
primaryAction,
|
|
86
|
+
secondaryAction,
|
|
87
|
+
stats = [],
|
|
88
|
+
floatingElements,
|
|
89
|
+
backgroundVariant = 'mesh-gradient',
|
|
90
|
+
backgroundIntensity = 'medium',
|
|
91
|
+
showBackgroundSwitcher = false,
|
|
92
|
+
codeCommand,
|
|
93
|
+
className
|
|
94
|
+
}) => {
|
|
95
|
+
const [currentVariant, setCurrentVariant] = React.useState<BackgroundVariant>(backgroundVariant);
|
|
96
|
+
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
|
|
97
|
+
const [heroTheme, setHeroTheme] = React.useState<'light' | 'dark'>('dark');
|
|
98
|
+
const { copyToClipboard } = useCopy();
|
|
99
|
+
|
|
100
|
+
// Show background switcher in development mode or if explicitly enabled
|
|
101
|
+
const isDevelopment = process.env.NODE_ENV === 'development';
|
|
102
|
+
const shouldShowSwitcher = showBackgroundSwitcher || isDevelopment;
|
|
103
|
+
|
|
104
|
+
const cycleBackground = () => {
|
|
105
|
+
const currentIndex = BACKGROUND_VARIANTS.indexOf(currentVariant);
|
|
106
|
+
const nextIndex = (currentIndex + 1) % BACKGROUND_VARIANTS.length;
|
|
107
|
+
setCurrentVariant(BACKGROUND_VARIANTS[nextIndex]);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<ForceTheme theme={heroTheme}>
|
|
112
|
+
<section
|
|
113
|
+
className={cn(
|
|
114
|
+
"relative overflow-hidden bg-background text-foreground z-0 isolate",
|
|
115
|
+
className
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{/* Animated Background */}
|
|
119
|
+
<div className="absolute inset-0 -z-10">
|
|
120
|
+
<AnimatedBackground variant={currentVariant} intensity={backgroundIntensity} />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 sm:py-24 lg:py-32 relative z-10">
|
|
124
|
+
<div className="text-center">
|
|
125
|
+
{/* Enhanced Badge */}
|
|
126
|
+
{badge && (
|
|
127
|
+
<div className="inline-flex items-center gap-2 px-4 py-2 bg-primary/10 text-primary rounded-full text-sm font-medium mb-6 border border-primary/20 hover:bg-primary/20 transition-all duration-300">
|
|
128
|
+
{badge.icon || <Sparkles className="w-4 h-4" />}
|
|
129
|
+
{badge.text}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{/* Enhanced Main Title */}
|
|
134
|
+
<h1 className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl font-bold text-foreground mb-6 leading-tight">
|
|
135
|
+
{title.split('\n').map((line, index) => (
|
|
136
|
+
<React.Fragment key={index}>
|
|
137
|
+
{line}
|
|
138
|
+
{index < title.split('\n').length - 1 && <br />}
|
|
139
|
+
</React.Fragment>
|
|
140
|
+
))}
|
|
141
|
+
{titleGradient && (
|
|
142
|
+
<>
|
|
143
|
+
<br />
|
|
144
|
+
<span className="bg-gradient-to-r from-primary via-primary/80 to-secondary bg-clip-text text-transparent animate-pulse">
|
|
145
|
+
{titleGradient}
|
|
146
|
+
</span>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</h1>
|
|
150
|
+
|
|
151
|
+
{/* Enhanced Subtitle */}
|
|
152
|
+
<p className="text-xl sm:text-2xl lg:text-3xl text-muted-foreground mb-8 max-w-4xl mx-auto leading-relaxed">
|
|
153
|
+
{subtitle}
|
|
154
|
+
</p>
|
|
155
|
+
|
|
156
|
+
{/* Code Command */}
|
|
157
|
+
{codeCommand && (
|
|
158
|
+
<div className="flex justify-center mb-8">
|
|
159
|
+
<div className="inline-flex items-center gap-3 px-6 py-3 bg-background/50 backdrop-blur-md border border-primary/20 rounded-xl shadow-lg hover:border-primary/40 transition-colors group cursor-pointer"
|
|
160
|
+
onClick={() => copyToClipboard(codeCommand, 'Command copied!')}>
|
|
161
|
+
<code className="font-mono text-lg text-primary font-semibold">
|
|
162
|
+
{codeCommand}
|
|
163
|
+
</code>
|
|
164
|
+
<div className="p-1.5 rounded-md bg-primary/10 text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
|
|
165
|
+
<Copy className="w-4 h-4" />
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Enhanced Features */}
|
|
172
|
+
{features.length > 0 && (
|
|
173
|
+
<div className="flex flex-wrap justify-center gap-6 mb-8">
|
|
174
|
+
{features.map((feature, index) => (
|
|
175
|
+
<div
|
|
176
|
+
key={index}
|
|
177
|
+
className="flex items-center gap-2 text-sm text-muted-foreground bg-card/50 px-4 py-2 rounded-full border border-border/50 hover:border-primary/30 hover:bg-card transition-all duration-300"
|
|
178
|
+
>
|
|
179
|
+
{feature.icon}
|
|
180
|
+
<span>{feature.text}</span>
|
|
181
|
+
</div>
|
|
182
|
+
))}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{/* Enhanced CTA Buttons */}
|
|
187
|
+
{(primaryAction || secondaryAction) && (
|
|
188
|
+
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-12">
|
|
189
|
+
{primaryAction && (
|
|
190
|
+
<ButtonLink
|
|
191
|
+
href={primaryAction.href || '#'}
|
|
192
|
+
onClick={primaryAction.onClick}
|
|
193
|
+
size="huge"
|
|
194
|
+
className="group"
|
|
195
|
+
>
|
|
196
|
+
{primaryAction.label}
|
|
197
|
+
<ArrowRight className="group-hover:translate-x-1 transition-transform duration-300" />
|
|
198
|
+
</ButtonLink>
|
|
199
|
+
)}
|
|
200
|
+
{secondaryAction && (
|
|
201
|
+
<ButtonLink
|
|
202
|
+
href={secondaryAction.href || '#'}
|
|
203
|
+
onClick={secondaryAction.onClick}
|
|
204
|
+
variant="outline"
|
|
205
|
+
size="huge"
|
|
206
|
+
className="group"
|
|
207
|
+
>
|
|
208
|
+
{secondaryAction.label}
|
|
209
|
+
{secondaryAction.icon && (
|
|
210
|
+
<span className="group-hover:rotate-12 transition-transform duration-300">
|
|
211
|
+
{secondaryAction.icon}
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
</ButtonLink>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{/* Enhanced Stats Preview */}
|
|
220
|
+
{stats.length > 0 && (
|
|
221
|
+
<div className="flex flex-wrap justify-center gap-x-6 gap-y-3 md:gap-x-8 md:gap-y-4 max-w-3xl mx-auto">
|
|
222
|
+
{stats.map((stat, index) => (
|
|
223
|
+
<div
|
|
224
|
+
key={index}
|
|
225
|
+
className="text-center min-w-[80px] md:min-w-[100px] px-4 py-2 md:px-5 md:py-3 rounded-lg border border-foreground/8 bg-background/10 backdrop-blur-sm"
|
|
226
|
+
>
|
|
227
|
+
<div className="text-lg md:text-xl font-bold text-foreground mb-0.5 md:mb-1">
|
|
228
|
+
{stat.number}
|
|
229
|
+
</div>
|
|
230
|
+
<div className="text-[10px] md:text-xs text-foreground/50 uppercase tracking-wide">
|
|
231
|
+
{stat.label}
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{/* Floating Elements */}
|
|
241
|
+
{floatingElements}
|
|
242
|
+
|
|
243
|
+
{/* Sticky Background Switcher Bar */}
|
|
244
|
+
{shouldShowSwitcher && (
|
|
245
|
+
<Sticky
|
|
246
|
+
bottom
|
|
247
|
+
offsetBottom={32}
|
|
248
|
+
className="w-full flex justify-center pointer-events-none"
|
|
249
|
+
zIndex={50}
|
|
250
|
+
>
|
|
251
|
+
<div className="inline-flex items-center gap-2 px-4 py-2 bg-background/90 backdrop-blur-md border border-border rounded-full shadow-lg pointer-events-auto">
|
|
252
|
+
{/* Current Background Label */}
|
|
253
|
+
<span className="text-xs text-muted-foreground font-medium">
|
|
254
|
+
{VARIANT_LABELS[currentVariant] || currentVariant}
|
|
255
|
+
</span>
|
|
256
|
+
|
|
257
|
+
{/* Separator */}
|
|
258
|
+
<div className="w-px h-4 bg-border" />
|
|
259
|
+
|
|
260
|
+
{/* Cycle Background Button */}
|
|
261
|
+
<Tooltip>
|
|
262
|
+
<TooltipTrigger asChild>
|
|
263
|
+
<Button
|
|
264
|
+
size="sm"
|
|
265
|
+
variant="ghost"
|
|
266
|
+
onClick={cycleBackground}
|
|
267
|
+
className="h-7 w-7 p-0"
|
|
268
|
+
>
|
|
269
|
+
<Wand2 className="w-3.5 h-3.5" />
|
|
270
|
+
</Button>
|
|
271
|
+
</TooltipTrigger>
|
|
272
|
+
<TooltipContent>
|
|
273
|
+
<p>Cycle background</p>
|
|
274
|
+
</TooltipContent>
|
|
275
|
+
</Tooltip>
|
|
276
|
+
|
|
277
|
+
{/* Background Menu */}
|
|
278
|
+
<div className="relative">
|
|
279
|
+
{isMenuOpen && (
|
|
280
|
+
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-card border border-border rounded-lg shadow-xl p-2 min-w-[180px]">
|
|
281
|
+
<div className="text-xs font-semibold text-muted-foreground mb-2 px-2">
|
|
282
|
+
Background Style
|
|
283
|
+
</div>
|
|
284
|
+
<div className="space-y-1">
|
|
285
|
+
{BACKGROUND_VARIANTS.map((variant) => (
|
|
286
|
+
<button
|
|
287
|
+
key={variant}
|
|
288
|
+
onClick={() => {
|
|
289
|
+
setCurrentVariant(variant);
|
|
290
|
+
setIsMenuOpen(false);
|
|
291
|
+
}}
|
|
292
|
+
className={cn(
|
|
293
|
+
"w-full text-left px-3 py-2 rounded-md text-sm transition-colors",
|
|
294
|
+
currentVariant === variant
|
|
295
|
+
? "bg-primary text-primary-foreground"
|
|
296
|
+
: "hover:bg-accent hover:text-accent-foreground"
|
|
297
|
+
)}
|
|
298
|
+
>
|
|
299
|
+
{VARIANT_LABELS[variant] || variant}
|
|
300
|
+
</button>
|
|
301
|
+
))}
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
|
|
306
|
+
<Tooltip>
|
|
307
|
+
<TooltipTrigger asChild>
|
|
308
|
+
<Button
|
|
309
|
+
size="sm"
|
|
310
|
+
variant="ghost"
|
|
311
|
+
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
|
312
|
+
className="h-7 w-7 p-0"
|
|
313
|
+
>
|
|
314
|
+
<Sparkles className="w-3.5 h-3.5" />
|
|
315
|
+
</Button>
|
|
316
|
+
</TooltipTrigger>
|
|
317
|
+
<TooltipContent>
|
|
318
|
+
<p>Choose background</p>
|
|
319
|
+
</TooltipContent>
|
|
320
|
+
</Tooltip>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</Sticky>
|
|
324
|
+
)}
|
|
325
|
+
</section>
|
|
326
|
+
</ForceTheme>
|
|
327
|
+
);
|
|
328
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent } from '@djangocfg/ui-core/components';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
interface Testimonial {
|
|
7
|
+
content: string;
|
|
8
|
+
author: {
|
|
9
|
+
name: string;
|
|
10
|
+
title?: string;
|
|
11
|
+
company?: string;
|
|
12
|
+
avatar?: string;
|
|
13
|
+
};
|
|
14
|
+
rating?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TestimonialSectionProps {
|
|
18
|
+
title?: string;
|
|
19
|
+
subtitle?: string;
|
|
20
|
+
testimonials: Testimonial[];
|
|
21
|
+
columns?: 1 | 2 | 3;
|
|
22
|
+
className?: string;
|
|
23
|
+
background?: 'dark' | 'card' | 'gradient';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const TestimonialSection: React.FC<TestimonialSectionProps> = ({
|
|
27
|
+
title,
|
|
28
|
+
subtitle,
|
|
29
|
+
testimonials,
|
|
30
|
+
columns = 3,
|
|
31
|
+
className,
|
|
32
|
+
background = 'dark'
|
|
33
|
+
}) => {
|
|
34
|
+
// Simple Tailwind 4 classes only - no custom utilities
|
|
35
|
+
const gridClasses = {
|
|
36
|
+
1: 'grid-cols-1 max-w-4xl mx-auto',
|
|
37
|
+
2: 'grid-cols-1 md:grid-cols-2',
|
|
38
|
+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const backgroundClasses = {
|
|
42
|
+
dark: 'bg-background',
|
|
43
|
+
card: 'bg-muted/30',
|
|
44
|
+
gradient: 'bg-gradient-to-b from-background via-muted/10 to-background',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const renderStars = (rating: number) => {
|
|
48
|
+
return Array.from({ length: 5 }, (_, i) => (
|
|
49
|
+
<span key={i} className={i < rating ? 'text-yellow-400' : 'text-neutral-400'}>
|
|
50
|
+
★
|
|
51
|
+
</span>
|
|
52
|
+
));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<section className={cn(
|
|
57
|
+
'relative py-16 sm:py-20 md:py-24 lg:py-32',
|
|
58
|
+
backgroundClasses[background],
|
|
59
|
+
className
|
|
60
|
+
)}>
|
|
61
|
+
<div className="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
62
|
+
{(title || subtitle) && (
|
|
63
|
+
<div className="text-center mb-12 sm:mb-14 md:mb-16">
|
|
64
|
+
{title && (
|
|
65
|
+
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold text-foreground mb-4 sm:mb-6">
|
|
66
|
+
{title}
|
|
67
|
+
</h2>
|
|
68
|
+
)}
|
|
69
|
+
{subtitle && (
|
|
70
|
+
<p className="text-lg sm:text-xl text-muted-foreground max-w-3xl mx-auto">
|
|
71
|
+
{subtitle}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<div className={cn('grid gap-6 sm:gap-8', gridClasses[columns])}>
|
|
78
|
+
{testimonials.map((testimonial, index) => (
|
|
79
|
+
<Card
|
|
80
|
+
key={index}
|
|
81
|
+
className="h-full hover:shadow-lg transition-all duration-300 backdrop-blur-sm border-border/50 hover:border-primary/30 group"
|
|
82
|
+
>
|
|
83
|
+
<CardContent className="p-8">
|
|
84
|
+
{testimonial.rating && (
|
|
85
|
+
<div className="flex mb-6 text-lg">
|
|
86
|
+
{renderStars(testimonial.rating)}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<blockquote className="text-foreground mb-8 italic text-lg leading-relaxed group-hover:text-primary transition-colors duration-300">
|
|
91
|
+
"{testimonial.content}"
|
|
92
|
+
</blockquote>
|
|
93
|
+
|
|
94
|
+
<div className="flex items-center">
|
|
95
|
+
{testimonial.author.avatar && (
|
|
96
|
+
<img
|
|
97
|
+
src={testimonial.author.avatar}
|
|
98
|
+
alt={testimonial.author.name}
|
|
99
|
+
className="w-14 h-14 rounded-full mr-4 ring-2 ring-primary/20 group-hover:ring-primary/40 transition-all duration-300"
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<div>
|
|
104
|
+
<div className="font-semibold text-foreground group-hover:text-primary transition-colors duration-300">
|
|
105
|
+
{testimonial.author.name}
|
|
106
|
+
</div>
|
|
107
|
+
{testimonial.author.title && (
|
|
108
|
+
<div className="text-sm text-muted-foreground">
|
|
109
|
+
{testimonial.author.title}
|
|
110
|
+
{testimonial.author.company && ` at ${testimonial.author.company}`}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</CardContent>
|
|
116
|
+
</Card>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</section>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './ArticleCard';
|
|
2
|
+
export * from './ArticleList';
|
|
3
|
+
export * from './CTASection';
|
|
4
|
+
export * from './FeatureSection';
|
|
5
|
+
export * from './Hero';
|
|
6
|
+
export * from './NewsletterSection';
|
|
7
|
+
export * from './StatsSection';
|
|
8
|
+
export * from './SuperHero';
|
|
9
|
+
export * from './TestimonialSection';
|