@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,94 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
3
|
+
import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, ButtonLink } from '@djangocfg/ui-core/components';
|
|
4
|
+
|
|
5
|
+
export type ArticleType = 'security' | 'release' | 'announcement' | 'feature';
|
|
6
|
+
|
|
7
|
+
interface ArticleCardProps {
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
date: string;
|
|
11
|
+
type: ArticleType;
|
|
12
|
+
href: string;
|
|
13
|
+
author?: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
featured?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const typeConfig: Record<ArticleType, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline'; icon: string }> = {
|
|
20
|
+
security: { label: 'Security', variant: 'destructive', icon: '🛡️' },
|
|
21
|
+
release: { label: 'Release', variant: 'default', icon: '📦' },
|
|
22
|
+
announcement: { label: 'Announcement', variant: 'secondary', icon: '📢' },
|
|
23
|
+
feature: { label: 'Feature', variant: 'outline', icon: '✨' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const ArticleCard: React.FC<ArticleCardProps> = ({
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
date,
|
|
30
|
+
type,
|
|
31
|
+
href,
|
|
32
|
+
author,
|
|
33
|
+
tags,
|
|
34
|
+
featured = false,
|
|
35
|
+
className,
|
|
36
|
+
}) => {
|
|
37
|
+
const config = typeConfig[type];
|
|
38
|
+
|
|
39
|
+
const formattedDate = new Date(date).toLocaleDateString('en-US', {
|
|
40
|
+
year: 'numeric',
|
|
41
|
+
month: 'long',
|
|
42
|
+
day: 'numeric',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Card className={cn(
|
|
47
|
+
'group relative overflow-hidden transition-all duration-200 hover:shadow-lg',
|
|
48
|
+
featured && 'border-primary/50 bg-primary/5',
|
|
49
|
+
className
|
|
50
|
+
)}>
|
|
51
|
+
<CardHeader className="pb-3">
|
|
52
|
+
<div className="flex items-center justify-between gap-2 mb-2">
|
|
53
|
+
<div className="flex items-center gap-2">
|
|
54
|
+
<span className="text-lg">{config.icon}</span>
|
|
55
|
+
<Badge variant={config.variant}>{config.label}</Badge>
|
|
56
|
+
</div>
|
|
57
|
+
<time className="text-sm text-muted-foreground">{formattedDate}</time>
|
|
58
|
+
</div>
|
|
59
|
+
<CardTitle className="text-xl group-hover:text-primary transition-colors line-clamp-2">
|
|
60
|
+
<a href={href} className="hover:underline">
|
|
61
|
+
{title}
|
|
62
|
+
</a>
|
|
63
|
+
</CardTitle>
|
|
64
|
+
{description && (
|
|
65
|
+
<CardDescription className="line-clamp-2 mt-2">
|
|
66
|
+
{description}
|
|
67
|
+
</CardDescription>
|
|
68
|
+
)}
|
|
69
|
+
</CardHeader>
|
|
70
|
+
|
|
71
|
+
<CardContent className="pt-0">
|
|
72
|
+
<div className="flex items-center justify-between">
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
{author && (
|
|
75
|
+
<span className="text-sm text-muted-foreground">by {author}</span>
|
|
76
|
+
)}
|
|
77
|
+
{tags && tags.length > 0 && (
|
|
78
|
+
<div className="flex gap-1">
|
|
79
|
+
{tags.slice(0, 2).map((tag) => (
|
|
80
|
+
<Badge key={tag} variant="outline" className="text-xs">
|
|
81
|
+
{tag}
|
|
82
|
+
</Badge>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
<ButtonLink href={href} variant="ghost" size="sm">
|
|
88
|
+
Read more →
|
|
89
|
+
</ButtonLink>
|
|
90
|
+
</div>
|
|
91
|
+
</CardContent>
|
|
92
|
+
</Card>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
3
|
+
import { ArticleCard, ArticleType } from './ArticleCard';
|
|
4
|
+
|
|
5
|
+
export interface Article {
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
date: string;
|
|
9
|
+
type: ArticleType;
|
|
10
|
+
href: string;
|
|
11
|
+
author?: string;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
featured?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ArticleListProps {
|
|
17
|
+
articles: Article[];
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
showFeatured?: boolean;
|
|
21
|
+
columns?: 1 | 2 | 3;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const ArticleList: React.FC<ArticleListProps> = ({
|
|
26
|
+
articles,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
showFeatured = true,
|
|
30
|
+
columns = 2,
|
|
31
|
+
className,
|
|
32
|
+
}) => {
|
|
33
|
+
// Sort by date, newest first
|
|
34
|
+
const sortedArticles = [...articles].sort(
|
|
35
|
+
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Separate featured and regular articles
|
|
39
|
+
const featuredArticles = showFeatured
|
|
40
|
+
? sortedArticles.filter((a) => a.featured)
|
|
41
|
+
: [];
|
|
42
|
+
const regularArticles = showFeatured
|
|
43
|
+
? sortedArticles.filter((a) => !a.featured)
|
|
44
|
+
: sortedArticles;
|
|
45
|
+
|
|
46
|
+
const gridCols = {
|
|
47
|
+
1: 'grid-cols-1',
|
|
48
|
+
2: 'grid-cols-1 md:grid-cols-2',
|
|
49
|
+
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<section className={cn('py-8', className)}>
|
|
54
|
+
{(title || description) && (
|
|
55
|
+
<div className="mb-8">
|
|
56
|
+
{title && (
|
|
57
|
+
<h2 className="text-3xl font-bold tracking-tight">{title}</h2>
|
|
58
|
+
)}
|
|
59
|
+
{description && (
|
|
60
|
+
<p className="mt-2 text-lg text-muted-foreground">{description}</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{/* Featured Articles */}
|
|
66
|
+
{featuredArticles.length > 0 && (
|
|
67
|
+
<div className="mb-8">
|
|
68
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
|
|
69
|
+
Featured
|
|
70
|
+
</h3>
|
|
71
|
+
<div className="grid gap-4">
|
|
72
|
+
{featuredArticles.map((article, index) => (
|
|
73
|
+
<ArticleCard key={index} {...article} featured />
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Regular Articles */}
|
|
80
|
+
{regularArticles.length > 0 && (
|
|
81
|
+
<div className={cn('grid gap-4', gridCols[columns])}>
|
|
82
|
+
{regularArticles.map((article, index) => (
|
|
83
|
+
<ArticleCard key={index} {...article} />
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{articles.length === 0 && (
|
|
89
|
+
<div className="text-center py-12 text-muted-foreground">
|
|
90
|
+
No articles yet. Check back soon!
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</section>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
|
|
9
|
+
interface CTAButton {
|
|
10
|
+
label: string;
|
|
11
|
+
href?: string;
|
|
12
|
+
onClick?: () => void;
|
|
13
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
14
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CTASectionProps {
|
|
18
|
+
title: string;
|
|
19
|
+
subtitle?: string;
|
|
20
|
+
primaryCTA?: CTAButton;
|
|
21
|
+
secondaryCTA?: CTAButton;
|
|
22
|
+
background?: 'default' | 'muted' | 'primary' | 'gradient';
|
|
23
|
+
className?: string;
|
|
24
|
+
children?: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const CTASection: React.FC<CTASectionProps> = ({
|
|
28
|
+
title,
|
|
29
|
+
subtitle,
|
|
30
|
+
primaryCTA,
|
|
31
|
+
secondaryCTA,
|
|
32
|
+
background = 'default',
|
|
33
|
+
className,
|
|
34
|
+
children
|
|
35
|
+
}) => {
|
|
36
|
+
// Simple Tailwind 4 classes - no custom utilities
|
|
37
|
+
const backgroundClasses = {
|
|
38
|
+
default: 'bg-background',
|
|
39
|
+
muted: 'bg-muted/30',
|
|
40
|
+
primary: 'bg-primary/5',
|
|
41
|
+
gradient: 'bg-gradient-to-b from-background via-primary/5 to-background',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<section
|
|
46
|
+
className={cn(
|
|
47
|
+
'relative py-16 sm:py-20 md:py-24 lg:py-32',
|
|
48
|
+
backgroundClasses[background],
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{/* Simple decorative background - using only Tailwind classes */}
|
|
53
|
+
{background === 'gradient' && (
|
|
54
|
+
<div className="absolute inset-0 -z-10 overflow-hidden pointer-events-none" aria-hidden="true">
|
|
55
|
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-4xl aspect-square bg-primary/10 rounded-full blur-3xl" />
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
<div className="container max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
60
|
+
<div className="text-center space-y-6 sm:space-y-8">
|
|
61
|
+
{/* Title */}
|
|
62
|
+
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight">
|
|
63
|
+
{title}
|
|
64
|
+
</h2>
|
|
65
|
+
|
|
66
|
+
{/* Subtitle */}
|
|
67
|
+
{subtitle && (
|
|
68
|
+
<p className="text-base sm:text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
|
69
|
+
{subtitle}
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{/* CTA Buttons */}
|
|
74
|
+
{(primaryCTA || secondaryCTA) && (
|
|
75
|
+
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center items-center pt-4">
|
|
76
|
+
{primaryCTA && (
|
|
77
|
+
primaryCTA.onClick ? (
|
|
78
|
+
<Button
|
|
79
|
+
onClick={primaryCTA.onClick}
|
|
80
|
+
variant={primaryCTA.variant || 'default'}
|
|
81
|
+
size={primaryCTA.size || 'lg'}
|
|
82
|
+
className="w-full sm:w-auto"
|
|
83
|
+
>
|
|
84
|
+
{primaryCTA.label}
|
|
85
|
+
</Button>
|
|
86
|
+
) : (
|
|
87
|
+
<Button
|
|
88
|
+
asChild
|
|
89
|
+
variant={primaryCTA.variant || 'default'}
|
|
90
|
+
size={primaryCTA.size || 'lg'}
|
|
91
|
+
className="w-full sm:w-auto"
|
|
92
|
+
>
|
|
93
|
+
<Link href={primaryCTA.href || '#'}>
|
|
94
|
+
{primaryCTA.label}
|
|
95
|
+
</Link>
|
|
96
|
+
</Button>
|
|
97
|
+
)
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{secondaryCTA && (
|
|
101
|
+
secondaryCTA.onClick ? (
|
|
102
|
+
<Button
|
|
103
|
+
onClick={secondaryCTA.onClick}
|
|
104
|
+
variant={secondaryCTA.variant || 'outline'}
|
|
105
|
+
size={secondaryCTA.size || 'lg'}
|
|
106
|
+
className="w-full sm:w-auto"
|
|
107
|
+
>
|
|
108
|
+
{secondaryCTA.label}
|
|
109
|
+
</Button>
|
|
110
|
+
) : (
|
|
111
|
+
<Button
|
|
112
|
+
asChild
|
|
113
|
+
variant={secondaryCTA.variant || 'outline'}
|
|
114
|
+
size={secondaryCTA.size || 'lg'}
|
|
115
|
+
className="w-full sm:w-auto"
|
|
116
|
+
>
|
|
117
|
+
<Link href={secondaryCTA.href || '#'}>
|
|
118
|
+
{secondaryCTA.label}
|
|
119
|
+
</Link>
|
|
120
|
+
</Button>
|
|
121
|
+
)
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{/* Optional children content */}
|
|
127
|
+
{children && (
|
|
128
|
+
<div className="pt-8 sm:pt-12">
|
|
129
|
+
{children}
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</section>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@djangocfg/ui-core/components';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
interface Feature {
|
|
7
|
+
icon?: React.ReactNode;
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
gradient?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface FeatureSectionProps {
|
|
14
|
+
title: string;
|
|
15
|
+
subtitle?: string;
|
|
16
|
+
features: Feature[];
|
|
17
|
+
columns?: 1 | 2 | 3 | 4;
|
|
18
|
+
className?: string;
|
|
19
|
+
background?: 'dark' | 'card' | 'gradient';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const FeatureSection: React.FC<FeatureSectionProps> = ({
|
|
23
|
+
title,
|
|
24
|
+
subtitle,
|
|
25
|
+
features,
|
|
26
|
+
columns = 3,
|
|
27
|
+
className,
|
|
28
|
+
background = 'dark'
|
|
29
|
+
}) => {
|
|
30
|
+
const getGridClasses = () => {
|
|
31
|
+
switch (columns) {
|
|
32
|
+
case 1:
|
|
33
|
+
return 'grid-cols-1 max-w-2xl mx-auto';
|
|
34
|
+
case 2:
|
|
35
|
+
return 'grid-cols-1 sm:grid-cols-2';
|
|
36
|
+
case 3:
|
|
37
|
+
return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3';
|
|
38
|
+
case 4:
|
|
39
|
+
return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4';
|
|
40
|
+
default:
|
|
41
|
+
return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3';
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const getBackgroundClasses = () => {
|
|
46
|
+
switch (background) {
|
|
47
|
+
case 'dark':
|
|
48
|
+
return 'bg-muted/30';
|
|
49
|
+
case 'card':
|
|
50
|
+
return 'bg-card';
|
|
51
|
+
case 'gradient':
|
|
52
|
+
return 'bg-gradient-to-b from-background to-muted/20';
|
|
53
|
+
default:
|
|
54
|
+
return 'bg-background';
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<section className={cn('py-12 sm:py-16 lg:py-24', getBackgroundClasses(), className)}>
|
|
60
|
+
<div className="w-full px-4 sm:px-6 lg:px-8">
|
|
61
|
+
<div className="text-center mb-12 sm:mb-16">
|
|
62
|
+
<h2 className="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold text-foreground mb-4 sm:mb-6">
|
|
63
|
+
{title}
|
|
64
|
+
</h2>
|
|
65
|
+
{subtitle && (
|
|
66
|
+
<p className="text-base sm:text-lg md:text-xl text-muted-foreground max-w-3xl mx-auto px-2">
|
|
67
|
+
{subtitle}
|
|
68
|
+
</p>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className={cn('grid gap-6 sm:gap-8', getGridClasses())}>
|
|
73
|
+
{features.map((feature, index) => (
|
|
74
|
+
<Card
|
|
75
|
+
key={index}
|
|
76
|
+
className="h-full hover:shadow-lg transition-all duration-300 backdrop-blur-sm border-border/50 hover:border-primary/30 group"
|
|
77
|
+
>
|
|
78
|
+
<CardHeader className="pb-4">
|
|
79
|
+
{feature.icon && (
|
|
80
|
+
<div className={cn(
|
|
81
|
+
'w-12 h-12 sm:w-16 sm:h-16 rounded-lg flex items-center justify-center mb-4 sm:mb-6 group-hover:scale-110 transition-all duration-300',
|
|
82
|
+
feature.gradient || 'bg-primary/10 text-primary group-hover:bg-primary/20'
|
|
83
|
+
)}>
|
|
84
|
+
<div className="text-xl sm:text-2xl">
|
|
85
|
+
{feature.icon}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
<CardTitle className="text-lg sm:text-xl text-foreground group-hover:text-primary transition-colors duration-300">
|
|
90
|
+
{feature.title}
|
|
91
|
+
</CardTitle>
|
|
92
|
+
</CardHeader>
|
|
93
|
+
<CardContent>
|
|
94
|
+
<CardDescription className="text-sm sm:text-base text-muted-foreground leading-relaxed">
|
|
95
|
+
{feature.description}
|
|
96
|
+
</CardDescription>
|
|
97
|
+
</CardContent>
|
|
98
|
+
</Card>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { ButtonLink } from '@djangocfg/ui-core/components';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
interface HeroProps {
|
|
7
|
+
title: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
primaryAction?: {
|
|
11
|
+
label: string;
|
|
12
|
+
href: string;
|
|
13
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
14
|
+
};
|
|
15
|
+
secondaryAction?: {
|
|
16
|
+
label: string;
|
|
17
|
+
href: string;
|
|
18
|
+
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
19
|
+
};
|
|
20
|
+
background?: 'gradient' | 'solid' | 'image' | 'dark';
|
|
21
|
+
className?: string;
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Hero: React.FC<HeroProps> = ({
|
|
26
|
+
title,
|
|
27
|
+
subtitle,
|
|
28
|
+
description,
|
|
29
|
+
primaryAction,
|
|
30
|
+
secondaryAction,
|
|
31
|
+
background = 'dark',
|
|
32
|
+
className,
|
|
33
|
+
children
|
|
34
|
+
}) => {
|
|
35
|
+
// Simple Tailwind 4 classes only - no custom utilities
|
|
36
|
+
const backgroundClasses = {
|
|
37
|
+
gradient: 'bg-gradient-to-b from-primary/10 to-background',
|
|
38
|
+
solid: 'bg-primary text-primary-foreground',
|
|
39
|
+
image: 'bg-cover bg-center bg-no-repeat text-white',
|
|
40
|
+
dark: 'bg-background text-foreground',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<section className={cn(
|
|
45
|
+
'relative py-16 sm:py-20 md:py-24 lg:py-32',
|
|
46
|
+
backgroundClasses[background],
|
|
47
|
+
className
|
|
48
|
+
)}>
|
|
49
|
+
<div className="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
50
|
+
<div className="text-center max-w-4xl mx-auto">
|
|
51
|
+
{subtitle && (
|
|
52
|
+
<p className="text-base sm:text-lg md:text-xl mb-4 sm:mb-6 text-muted-foreground font-medium">
|
|
53
|
+
{subtitle}
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
|
|
57
|
+
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold mb-6 sm:mb-8 leading-tight">
|
|
58
|
+
{title}
|
|
59
|
+
</h1>
|
|
60
|
+
|
|
61
|
+
{description && (
|
|
62
|
+
<p className="text-lg sm:text-xl md:text-2xl mb-8 sm:mb-10 md:mb-12 text-muted-foreground leading-relaxed max-w-3xl mx-auto">
|
|
63
|
+
{description}
|
|
64
|
+
</p>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{(primaryAction || secondaryAction) && (
|
|
68
|
+
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 justify-center">
|
|
69
|
+
{primaryAction && (
|
|
70
|
+
<ButtonLink
|
|
71
|
+
href={primaryAction.href}
|
|
72
|
+
variant={primaryAction.variant || 'default'}
|
|
73
|
+
size="huge"
|
|
74
|
+
className="w-full sm:w-auto"
|
|
75
|
+
>
|
|
76
|
+
{primaryAction.label}
|
|
77
|
+
</ButtonLink>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
{secondaryAction && (
|
|
81
|
+
<ButtonLink
|
|
82
|
+
href={secondaryAction.href}
|
|
83
|
+
variant={secondaryAction.variant || 'outline'}
|
|
84
|
+
size="huge"
|
|
85
|
+
className="w-full sm:w-auto"
|
|
86
|
+
>
|
|
87
|
+
{secondaryAction.label}
|
|
88
|
+
</ButtonLink>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{children && (
|
|
94
|
+
<div className="mt-12 sm:mt-14 md:mt-16 flex justify-center">
|
|
95
|
+
{children}
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Mail } from 'lucide-react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button, Input } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
|
|
9
|
+
interface NewsletterSectionProps {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
buttonText?: string;
|
|
14
|
+
disclaimer?: string;
|
|
15
|
+
background?: 'default' | 'muted' | 'primary' | 'gradient';
|
|
16
|
+
className?: string;
|
|
17
|
+
onSubmit?: (email: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* NewsletterSection - Email subscription block
|
|
22
|
+
* Fully responsive and theme-aware (light/dark mode)
|
|
23
|
+
* Follows Tailwind CSS v4 canons - no custom utilities
|
|
24
|
+
*/
|
|
25
|
+
export const NewsletterSection: React.FC<NewsletterSectionProps> = ({
|
|
26
|
+
title = "Stay Updated with Our Newsletter",
|
|
27
|
+
description = "Get the latest insights on AI, technology, and software development delivered to your inbox.",
|
|
28
|
+
placeholder = "Enter your email address",
|
|
29
|
+
buttonText = "Subscribe",
|
|
30
|
+
disclaimer = "No spam, unsubscribe at any time",
|
|
31
|
+
background = 'muted',
|
|
32
|
+
className,
|
|
33
|
+
onSubmit
|
|
34
|
+
}) => {
|
|
35
|
+
const [email, setEmail] = React.useState('');
|
|
36
|
+
|
|
37
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
if (email && onSubmit) {
|
|
40
|
+
onSubmit(email);
|
|
41
|
+
setEmail('');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Simple Tailwind 4 classes only - no custom utilities
|
|
46
|
+
// All variants work in both light and dark themes
|
|
47
|
+
const backgroundClasses = {
|
|
48
|
+
default: 'bg-background',
|
|
49
|
+
muted: 'bg-muted/50',
|
|
50
|
+
primary: 'bg-primary/5',
|
|
51
|
+
gradient: 'bg-gradient-to-b from-background via-primary/5 to-background',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<section className={cn(
|
|
56
|
+
'relative py-16 sm:py-20 md:py-24 lg:py-32',
|
|
57
|
+
backgroundClasses[background],
|
|
58
|
+
className
|
|
59
|
+
)}>
|
|
60
|
+
{/* Decorative background for gradient variant only */}
|
|
61
|
+
{background === 'gradient' && (
|
|
62
|
+
<div className="absolute inset-0 -z-10 overflow-hidden pointer-events-none" aria-hidden="true">
|
|
63
|
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-4xl aspect-square bg-primary/10 rounded-full blur-3xl" />
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<div className="container max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
68
|
+
<div className="text-center max-w-3xl mx-auto">
|
|
69
|
+
{/* Title */}
|
|
70
|
+
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight mb-4 sm:mb-6">
|
|
71
|
+
{title}
|
|
72
|
+
</h2>
|
|
73
|
+
|
|
74
|
+
{/* Description */}
|
|
75
|
+
{description && (
|
|
76
|
+
<p className="text-base sm:text-lg md:text-xl text-muted-foreground leading-relaxed mb-8 sm:mb-10 md:mb-12">
|
|
77
|
+
{description}
|
|
78
|
+
</p>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{/* Form */}
|
|
82
|
+
<form onSubmit={handleSubmit} className="max-w-md mx-auto">
|
|
83
|
+
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
|
84
|
+
{/* Email Input */}
|
|
85
|
+
<div className="relative flex-1">
|
|
86
|
+
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
|
87
|
+
<Input
|
|
88
|
+
type="email"
|
|
89
|
+
placeholder={placeholder}
|
|
90
|
+
value={email}
|
|
91
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
92
|
+
className="pl-10 h-12 w-full"
|
|
93
|
+
required
|
|
94
|
+
aria-label="Email address"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Submit Button */}
|
|
99
|
+
<Button
|
|
100
|
+
type="submit"
|
|
101
|
+
size="lg"
|
|
102
|
+
className="h-12 w-full sm:w-auto sm:min-w-[120px]"
|
|
103
|
+
>
|
|
104
|
+
{buttonText}
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Disclaimer */}
|
|
109
|
+
{disclaimer && (
|
|
110
|
+
<p className="text-xs sm:text-sm text-muted-foreground mt-3 text-center">
|
|
111
|
+
{disclaimer}
|
|
112
|
+
</p>
|
|
113
|
+
)}
|
|
114
|
+
</form>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</section>
|
|
118
|
+
);
|
|
119
|
+
};
|