@djangocfg/ui-nextjs 2.1.319 → 2.1.321
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/README.md +27 -262
- package/package.json +7 -17
- package/src/index.ts +11 -14
- package/src/animations/AnimatedBackground.tsx +0 -646
- package/src/animations/MouseFollower.tsx +0 -355
- package/src/animations/index.ts +0 -5
- package/src/blocks/ArticleCard.tsx +0 -94
- package/src/blocks/ArticleList.tsx +0 -96
- package/src/blocks/CTASection.tsx +0 -136
- package/src/blocks/FeatureSection.tsx +0 -176
- package/src/blocks/Hero.tsx +0 -102
- package/src/blocks/NewsletterSection.tsx +0 -119
- package/src/blocks/SplitHero/SplitHero.tsx +0 -95
- package/src/blocks/SplitHero/SplitHeroContent.tsx +0 -117
- package/src/blocks/SplitHero/SplitHeroMedia.tsx +0 -67
- package/src/blocks/SplitHero/index.ts +0 -13
- package/src/blocks/SplitHero/types.ts +0 -75
- package/src/blocks/StatsSection.tsx +0 -103
- package/src/blocks/SuperHero.tsx +0 -352
- package/src/blocks/TestimonialSection.tsx +0 -122
- package/src/blocks/index.ts +0 -10
- package/src/components/README.md +0 -2018
- package/src/components/breadcrumb-navigation.tsx +0 -130
- package/src/components/breadcrumb.tsx +0 -133
- package/src/components/dropdown-menu.tsx +0 -220
- package/src/components/index.ts +0 -52
- package/src/components/pagination-static.tsx +0 -344
- package/src/components/pagination.tsx +0 -138
- package/src/components/sidebar.tsx +0 -923
- package/src/components/ssr-pagination.tsx +0 -214
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
4
|
-
|
|
5
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
6
|
-
|
|
7
|
-
export type MouseFollowerVariant =
|
|
8
|
-
| 'glow'
|
|
9
|
-
| 'spotlight'
|
|
10
|
-
| 'gradient-blob'
|
|
11
|
-
| 'ring'
|
|
12
|
-
| 'trail';
|
|
13
|
-
|
|
14
|
-
interface MouseFollowerProps {
|
|
15
|
-
/** Visual style of the follower */
|
|
16
|
-
variant?: MouseFollowerVariant;
|
|
17
|
-
/** Size of the effect in pixels */
|
|
18
|
-
size?: number;
|
|
19
|
-
/** Color - can be CSS color or 'primary' to use theme */
|
|
20
|
-
color?: string;
|
|
21
|
-
/** Smoothness of following (0.05 = very smooth, 0.3 = snappy) */
|
|
22
|
-
smoothness?: number;
|
|
23
|
-
/** Opacity of the effect (0-1) */
|
|
24
|
-
opacity?: number;
|
|
25
|
-
/** Blur amount for glow effects */
|
|
26
|
-
blur?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl';
|
|
27
|
-
/** Additional className for the container */
|
|
28
|
-
className?: string;
|
|
29
|
-
/** Whether to show on mobile (touch devices) */
|
|
30
|
-
showOnMobile?: boolean;
|
|
31
|
-
/** Disable the effect */
|
|
32
|
-
disabled?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface Position {
|
|
36
|
-
x: number;
|
|
37
|
-
y: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const blurMap = {
|
|
41
|
-
sm: 'blur-sm',
|
|
42
|
-
md: 'blur-md',
|
|
43
|
-
lg: 'blur-lg',
|
|
44
|
-
xl: 'blur-xl',
|
|
45
|
-
'2xl': 'blur-2xl',
|
|
46
|
-
'3xl': 'blur-3xl',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const MouseFollower: React.FC<MouseFollowerProps> = ({
|
|
50
|
-
variant = 'glow',
|
|
51
|
-
size = 300,
|
|
52
|
-
color = 'primary',
|
|
53
|
-
smoothness = 0.1,
|
|
54
|
-
opacity = 0.3,
|
|
55
|
-
blur = '2xl',
|
|
56
|
-
className,
|
|
57
|
-
showOnMobile = false,
|
|
58
|
-
disabled = false,
|
|
59
|
-
}) => {
|
|
60
|
-
const [mounted, setMounted] = useState(false);
|
|
61
|
-
const [isVisible, setIsVisible] = useState(false);
|
|
62
|
-
const [isMobile, setIsMobile] = useState(false);
|
|
63
|
-
const positionRef = useRef<Position>({ x: 0, y: 0 });
|
|
64
|
-
const targetRef = useRef<Position>({ x: 0, y: 0 });
|
|
65
|
-
const elementRef = useRef<HTMLDivElement>(null);
|
|
66
|
-
const rafRef = useRef<number>(0);
|
|
67
|
-
|
|
68
|
-
// Resolve color
|
|
69
|
-
const resolvedColor = useMemo(() => {
|
|
70
|
-
if (color === 'primary') return 'hsl(var(--primary))';
|
|
71
|
-
if (color === 'secondary') return 'hsl(var(--secondary))';
|
|
72
|
-
if (color === 'accent') return 'hsl(var(--accent))';
|
|
73
|
-
return color;
|
|
74
|
-
}, [color]);
|
|
75
|
-
|
|
76
|
-
// Check if mobile
|
|
77
|
-
useEffect(() => {
|
|
78
|
-
setMounted(true);
|
|
79
|
-
const checkMobile = () => {
|
|
80
|
-
setIsMobile('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
|
81
|
-
};
|
|
82
|
-
checkMobile();
|
|
83
|
-
window.addEventListener('resize', checkMobile);
|
|
84
|
-
return () => window.removeEventListener('resize', checkMobile);
|
|
85
|
-
}, []);
|
|
86
|
-
|
|
87
|
-
// Animation loop with lerp
|
|
88
|
-
const animate = useCallback(() => {
|
|
89
|
-
const dx = targetRef.current.x - positionRef.current.x;
|
|
90
|
-
const dy = targetRef.current.y - positionRef.current.y;
|
|
91
|
-
|
|
92
|
-
positionRef.current.x += dx * smoothness;
|
|
93
|
-
positionRef.current.y += dy * smoothness;
|
|
94
|
-
|
|
95
|
-
if (elementRef.current) {
|
|
96
|
-
elementRef.current.style.transform = `translate(${positionRef.current.x - size / 2}px, ${positionRef.current.y - size / 2}px)`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
rafRef.current = requestAnimationFrame(animate);
|
|
100
|
-
}, [smoothness, size]);
|
|
101
|
-
|
|
102
|
-
// Mouse move handler
|
|
103
|
-
useEffect(() => {
|
|
104
|
-
if (disabled || !mounted) return;
|
|
105
|
-
if (isMobile && !showOnMobile) return;
|
|
106
|
-
|
|
107
|
-
const handleMouseMove = (e: MouseEvent) => {
|
|
108
|
-
targetRef.current = { x: e.clientX, y: e.clientY };
|
|
109
|
-
if (!isVisible) setIsVisible(true);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const handleMouseLeave = () => {
|
|
113
|
-
setIsVisible(false);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const handleMouseEnter = () => {
|
|
117
|
-
setIsVisible(true);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
121
|
-
document.addEventListener('mouseleave', handleMouseLeave);
|
|
122
|
-
document.addEventListener('mouseenter', handleMouseEnter);
|
|
123
|
-
|
|
124
|
-
rafRef.current = requestAnimationFrame(animate);
|
|
125
|
-
|
|
126
|
-
return () => {
|
|
127
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
128
|
-
document.removeEventListener('mouseleave', handleMouseLeave);
|
|
129
|
-
document.removeEventListener('mouseenter', handleMouseEnter);
|
|
130
|
-
cancelAnimationFrame(rafRef.current);
|
|
131
|
-
};
|
|
132
|
-
}, [disabled, mounted, isMobile, showOnMobile, animate, isVisible]);
|
|
133
|
-
|
|
134
|
-
if (!mounted || disabled) return null;
|
|
135
|
-
if (isMobile && !showOnMobile) return null;
|
|
136
|
-
|
|
137
|
-
return (
|
|
138
|
-
<div
|
|
139
|
-
className={cn(
|
|
140
|
-
'fixed inset-0 pointer-events-none overflow-hidden z-0',
|
|
141
|
-
className
|
|
142
|
-
)}
|
|
143
|
-
>
|
|
144
|
-
<div
|
|
145
|
-
ref={elementRef}
|
|
146
|
-
className={cn(
|
|
147
|
-
'absolute top-0 left-0 transition-opacity duration-300',
|
|
148
|
-
isVisible ? 'opacity-100' : 'opacity-0'
|
|
149
|
-
)}
|
|
150
|
-
style={{ width: size, height: size }}
|
|
151
|
-
>
|
|
152
|
-
{variant === 'glow' && (
|
|
153
|
-
<GlowEffect
|
|
154
|
-
size={size}
|
|
155
|
-
color={resolvedColor}
|
|
156
|
-
opacity={opacity}
|
|
157
|
-
blur={blurMap[blur]}
|
|
158
|
-
/>
|
|
159
|
-
)}
|
|
160
|
-
|
|
161
|
-
{variant === 'spotlight' && (
|
|
162
|
-
<SpotlightEffect
|
|
163
|
-
size={size}
|
|
164
|
-
color={resolvedColor}
|
|
165
|
-
opacity={opacity}
|
|
166
|
-
blur={blurMap[blur]}
|
|
167
|
-
/>
|
|
168
|
-
)}
|
|
169
|
-
|
|
170
|
-
{variant === 'gradient-blob' && (
|
|
171
|
-
<GradientBlobEffect
|
|
172
|
-
size={size}
|
|
173
|
-
color={resolvedColor}
|
|
174
|
-
opacity={opacity}
|
|
175
|
-
blur={blurMap[blur]}
|
|
176
|
-
/>
|
|
177
|
-
)}
|
|
178
|
-
|
|
179
|
-
{variant === 'ring' && (
|
|
180
|
-
<RingEffect
|
|
181
|
-
size={size}
|
|
182
|
-
color={resolvedColor}
|
|
183
|
-
opacity={opacity}
|
|
184
|
-
/>
|
|
185
|
-
)}
|
|
186
|
-
|
|
187
|
-
{variant === 'trail' && (
|
|
188
|
-
<TrailEffect
|
|
189
|
-
size={size}
|
|
190
|
-
color={resolvedColor}
|
|
191
|
-
opacity={opacity}
|
|
192
|
-
blur={blurMap[blur]}
|
|
193
|
-
/>
|
|
194
|
-
)}
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// =============================================================================
|
|
201
|
-
// Effect Variants
|
|
202
|
-
// =============================================================================
|
|
203
|
-
|
|
204
|
-
interface EffectProps {
|
|
205
|
-
size: number;
|
|
206
|
-
color: string;
|
|
207
|
-
opacity: number;
|
|
208
|
-
blur?: string;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const GlowEffect: React.FC<EffectProps> = ({ size: _size, color, opacity, blur }) => (
|
|
212
|
-
<div
|
|
213
|
-
className={cn('w-full h-full rounded-full', blur)}
|
|
214
|
-
style={{
|
|
215
|
-
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
|
216
|
-
opacity,
|
|
217
|
-
}}
|
|
218
|
-
/>
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const SpotlightEffect: React.FC<EffectProps> = ({ size: _size, color, opacity, blur }) => (
|
|
222
|
-
<>
|
|
223
|
-
{/* Main spotlight cone */}
|
|
224
|
-
<div
|
|
225
|
-
className={cn('absolute inset-0', blur)}
|
|
226
|
-
style={{
|
|
227
|
-
background: `conic-gradient(from 180deg at 50% 50%,
|
|
228
|
-
transparent 0deg,
|
|
229
|
-
${color} 150deg,
|
|
230
|
-
${color} 210deg,
|
|
231
|
-
transparent 360deg
|
|
232
|
-
)`,
|
|
233
|
-
opacity: opacity * 0.6,
|
|
234
|
-
}}
|
|
235
|
-
/>
|
|
236
|
-
{/* Center glow */}
|
|
237
|
-
<div
|
|
238
|
-
className={cn('absolute rounded-full', blur)}
|
|
239
|
-
style={{
|
|
240
|
-
width: '40%',
|
|
241
|
-
height: '40%',
|
|
242
|
-
top: '30%',
|
|
243
|
-
left: '30%',
|
|
244
|
-
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
|
245
|
-
opacity,
|
|
246
|
-
}}
|
|
247
|
-
/>
|
|
248
|
-
</>
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
const GradientBlobEffect: React.FC<EffectProps> = ({ size: _size, color, opacity, blur }) => (
|
|
252
|
-
<>
|
|
253
|
-
{/* Animated blob shape */}
|
|
254
|
-
<div
|
|
255
|
-
className={cn('w-full h-full', blur)}
|
|
256
|
-
style={{
|
|
257
|
-
background: `radial-gradient(ellipse 60% 40% at 50% 50%, ${color} 0%, transparent 70%)`,
|
|
258
|
-
opacity,
|
|
259
|
-
animation: 'blob-morph 8s ease-in-out infinite',
|
|
260
|
-
}}
|
|
261
|
-
/>
|
|
262
|
-
{/* Secondary blob */}
|
|
263
|
-
<div
|
|
264
|
-
className={cn('absolute inset-0', blur)}
|
|
265
|
-
style={{
|
|
266
|
-
background: `radial-gradient(ellipse 40% 60% at 50% 50%, ${color} 0%, transparent 60%)`,
|
|
267
|
-
opacity: opacity * 0.5,
|
|
268
|
-
animation: 'blob-morph 8s ease-in-out infinite reverse',
|
|
269
|
-
animationDelay: '-4s',
|
|
270
|
-
}}
|
|
271
|
-
/>
|
|
272
|
-
<style>{`
|
|
273
|
-
@keyframes blob-morph {
|
|
274
|
-
0%, 100% { transform: scale(1) rotate(0deg); border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; }
|
|
275
|
-
25% { transform: scale(1.1) rotate(90deg); border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; }
|
|
276
|
-
50% { transform: scale(0.9) rotate(180deg); border-radius: 50% 60% 30% 60% / 30% 60% 70% 40%; }
|
|
277
|
-
75% { transform: scale(1.05) rotate(270deg); border-radius: 60% 40% 50% 40% / 70% 30% 50% 60%; }
|
|
278
|
-
}
|
|
279
|
-
`}</style>
|
|
280
|
-
</>
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const RingEffect: React.FC<EffectProps> = ({ size: _size, color, opacity }) => (
|
|
284
|
-
<>
|
|
285
|
-
{/* Outer ring */}
|
|
286
|
-
<div
|
|
287
|
-
className="absolute inset-0 rounded-full"
|
|
288
|
-
style={{
|
|
289
|
-
border: `2px solid ${color}`,
|
|
290
|
-
opacity: opacity * 0.3,
|
|
291
|
-
}}
|
|
292
|
-
/>
|
|
293
|
-
{/* Middle ring */}
|
|
294
|
-
<div
|
|
295
|
-
className="absolute rounded-full"
|
|
296
|
-
style={{
|
|
297
|
-
inset: '15%',
|
|
298
|
-
border: `2px solid ${color}`,
|
|
299
|
-
opacity: opacity * 0.5,
|
|
300
|
-
animation: 'ring-pulse 2s ease-in-out infinite',
|
|
301
|
-
}}
|
|
302
|
-
/>
|
|
303
|
-
{/* Inner ring */}
|
|
304
|
-
<div
|
|
305
|
-
className="absolute rounded-full"
|
|
306
|
-
style={{
|
|
307
|
-
inset: '30%',
|
|
308
|
-
border: `2px solid ${color}`,
|
|
309
|
-
opacity: opacity * 0.7,
|
|
310
|
-
animation: 'ring-pulse 2s ease-in-out infinite',
|
|
311
|
-
animationDelay: '-1s',
|
|
312
|
-
}}
|
|
313
|
-
/>
|
|
314
|
-
{/* Center dot */}
|
|
315
|
-
<div
|
|
316
|
-
className="absolute rounded-full"
|
|
317
|
-
style={{
|
|
318
|
-
width: '8px',
|
|
319
|
-
height: '8px',
|
|
320
|
-
top: '50%',
|
|
321
|
-
left: '50%',
|
|
322
|
-
transform: 'translate(-50%, -50%)',
|
|
323
|
-
background: color,
|
|
324
|
-
opacity,
|
|
325
|
-
}}
|
|
326
|
-
/>
|
|
327
|
-
<style>{`
|
|
328
|
-
@keyframes ring-pulse {
|
|
329
|
-
0%, 100% { opacity: 0.3; transform: scale(1); }
|
|
330
|
-
50% { opacity: 0.7; transform: scale(1.05); }
|
|
331
|
-
}
|
|
332
|
-
`}</style>
|
|
333
|
-
</>
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
const TrailEffect: React.FC<EffectProps> = ({ size: _size, color, opacity, blur }) => (
|
|
337
|
-
<>
|
|
338
|
-
{/* Multiple trailing circles */}
|
|
339
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
340
|
-
<div
|
|
341
|
-
key={i}
|
|
342
|
-
className={cn('absolute rounded-full', blur)}
|
|
343
|
-
style={{
|
|
344
|
-
width: `${100 - i * 15}%`,
|
|
345
|
-
height: `${100 - i * 15}%`,
|
|
346
|
-
top: `${i * 7.5}%`,
|
|
347
|
-
left: `${i * 7.5}%`,
|
|
348
|
-
background: `radial-gradient(circle, ${color} 0%, transparent 70%)`,
|
|
349
|
-
opacity: opacity * (1 - i * 0.15),
|
|
350
|
-
transition: `transform ${0.1 + i * 0.05}s ease-out`,
|
|
351
|
-
}}
|
|
352
|
-
/>
|
|
353
|
-
))}
|
|
354
|
-
</>
|
|
355
|
-
);
|
package/src/animations/index.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import moment from 'moment';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
Badge, ButtonLink, Card, CardContent, CardDescription, CardHeader, CardTitle
|
|
6
|
-
} from '@djangocfg/ui-core/components';
|
|
7
|
-
|
|
8
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
|
-
|
|
10
|
-
export type ArticleType = 'security' | 'release' | 'announcement' | 'feature';
|
|
11
|
-
|
|
12
|
-
interface ArticleCardProps {
|
|
13
|
-
title: string;
|
|
14
|
-
description?: string;
|
|
15
|
-
date: string;
|
|
16
|
-
type: ArticleType;
|
|
17
|
-
href: string;
|
|
18
|
-
author?: string;
|
|
19
|
-
tags?: string[];
|
|
20
|
-
featured?: boolean;
|
|
21
|
-
className?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const typeConfig: Record<ArticleType, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline'; icon: string }> = {
|
|
25
|
-
security: { label: 'Security', variant: 'destructive', icon: '🛡️' },
|
|
26
|
-
release: { label: 'Release', variant: 'default', icon: '📦' },
|
|
27
|
-
announcement: { label: 'Announcement', variant: 'secondary', icon: '📢' },
|
|
28
|
-
feature: { label: 'Feature', variant: 'outline', icon: '✨' },
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const ArticleCard: React.FC<ArticleCardProps> = ({
|
|
32
|
-
title,
|
|
33
|
-
description,
|
|
34
|
-
date,
|
|
35
|
-
type,
|
|
36
|
-
href,
|
|
37
|
-
author,
|
|
38
|
-
tags,
|
|
39
|
-
featured = false,
|
|
40
|
-
className,
|
|
41
|
-
}) => {
|
|
42
|
-
const config = typeConfig[type];
|
|
43
|
-
const formattedDate = moment(date).format('MMMM D, YYYY');
|
|
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
|
-
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import moment from 'moment';
|
|
3
|
-
|
|
4
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
-
|
|
6
|
-
import { ArticleCard, ArticleType } from './ArticleCard';
|
|
7
|
-
|
|
8
|
-
export interface Article {
|
|
9
|
-
title: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
date: string;
|
|
12
|
-
type: ArticleType;
|
|
13
|
-
href: string;
|
|
14
|
-
author?: string;
|
|
15
|
-
tags?: string[];
|
|
16
|
-
featured?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface ArticleListProps {
|
|
20
|
-
articles: Article[];
|
|
21
|
-
title?: string;
|
|
22
|
-
description?: string;
|
|
23
|
-
showFeatured?: boolean;
|
|
24
|
-
columns?: 1 | 2 | 3;
|
|
25
|
-
className?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const ArticleList: React.FC<ArticleListProps> = ({
|
|
29
|
-
articles,
|
|
30
|
-
title,
|
|
31
|
-
description,
|
|
32
|
-
showFeatured = true,
|
|
33
|
-
columns = 2,
|
|
34
|
-
className,
|
|
35
|
-
}) => {
|
|
36
|
-
// Sort by date, newest first, and separate featured/regular articles
|
|
37
|
-
const { featuredArticles, regularArticles } = useMemo(() => {
|
|
38
|
-
const sorted = [...articles].sort(
|
|
39
|
-
(a, b) => moment(b.date).valueOf() - moment(a.date).valueOf()
|
|
40
|
-
);
|
|
41
|
-
return {
|
|
42
|
-
featuredArticles: showFeatured ? sorted.filter((a) => a.featured) : [],
|
|
43
|
-
regularArticles: showFeatured ? sorted.filter((a) => !a.featured) : sorted,
|
|
44
|
-
};
|
|
45
|
-
}, [articles, showFeatured]);
|
|
46
|
-
|
|
47
|
-
const gridCols = {
|
|
48
|
-
1: 'grid-cols-1',
|
|
49
|
-
2: 'grid-cols-1 md:grid-cols-2',
|
|
50
|
-
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<section className={cn('py-8', className)}>
|
|
55
|
-
{(title || description) && (
|
|
56
|
-
<div className="mb-8">
|
|
57
|
-
{title && (
|
|
58
|
-
<h2 className="text-3xl font-bold tracking-tight">{title}</h2>
|
|
59
|
-
)}
|
|
60
|
-
{description && (
|
|
61
|
-
<p className="mt-2 text-lg text-muted-foreground">{description}</p>
|
|
62
|
-
)}
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
|
|
66
|
-
{/* Featured Articles */}
|
|
67
|
-
{featuredArticles.length > 0 && (
|
|
68
|
-
<div className="mb-8">
|
|
69
|
-
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground mb-4">
|
|
70
|
-
Featured
|
|
71
|
-
</h3>
|
|
72
|
-
<div className="grid gap-4">
|
|
73
|
-
{featuredArticles.map((article, index) => (
|
|
74
|
-
<ArticleCard key={index} {...article} featured />
|
|
75
|
-
))}
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
)}
|
|
79
|
-
|
|
80
|
-
{/* Regular Articles */}
|
|
81
|
-
{regularArticles.length > 0 && (
|
|
82
|
-
<div className={cn('grid gap-4', gridCols[columns])}>
|
|
83
|
-
{regularArticles.map((article, index) => (
|
|
84
|
-
<ArticleCard key={index} {...article} />
|
|
85
|
-
))}
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
88
|
-
|
|
89
|
-
{articles.length === 0 && (
|
|
90
|
-
<div className="text-center py-12 text-muted-foreground">
|
|
91
|
-
No articles yet. Check back soon!
|
|
92
|
-
</div>
|
|
93
|
-
)}
|
|
94
|
-
</section>
|
|
95
|
-
);
|
|
96
|
-
};
|
|
@@ -1,136 +0,0 @@
|
|
|
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
|
-
};
|