blocks-dusted 0.1.11 → 0.1.12
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/package.json +5 -2
- package/src/cli.js +39 -3
- package/src/commands/shop.js +59 -0
- package/src/commands/warehouse.js +73 -0
- package/templates/blocks/DD-BookCall/Component.tsx +535 -534
- package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -139
- package/templates/blocks/DD-Carousel-A/Component.tsx +1 -1
- package/templates/blocks/DD-Carousel-B/Component.tsx +1 -1
- package/templates/blocks/DD-Chip/Component.tsx +14 -14
- package/templates/blocks/DD-ComparisonTable/Component.tsx +1 -1
- package/templates/blocks/DD-Contact/Component.tsx +8 -7
- package/templates/blocks/DD-Counter/Component.tsx +10 -10
- package/templates/blocks/DD-FeatCat/Component.tsx +1 -1
- package/templates/blocks/DD-FeatStrip/Component.tsx +1 -1
- package/templates/blocks/DD-Hero/Component.tsx +297 -320
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -938
- package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +8 -10
- package/templates/blocks/DD-Pricing/Component.tsx +369 -372
- package/templates/blocks/DD-Process/Component.tsx +147 -149
- package/templates/blocks/DD-Progress/Component.tsx +156 -141
- package/templates/blocks/DD-ProjSlider/config.ts +2 -2
- package/templates/blocks/DD-Section/Component.tsx +1 -1
- package/templates/blocks/DD-Services/Component.tsx +1 -1
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +13 -13
- package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
- package/templates/blocks/DD-StackCards/Component.tsx +473 -473
- package/templates/blocks/DD-Team/Component.tsx +1 -1
- package/templates/blocks/DD-TechStack/Component.tsx +22 -28
- package/templates/blocks/DD-Testimonials/Component.tsx +35 -43
- package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
- package/templates/blocks/DD-Thanks/Component.tsx +501 -500
- package/templates/blocks/DD-Work/Component.tsx +4 -4
- package/templates/blocks/DD-Work-B/Component.tsx +1 -1
- package/templates/blocks/DDHG-CTASection2/Component.tsx +6 -6
- package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -243
- package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -93
- package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
- package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -213
- package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
- package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
- package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +24 -28
- package/templates/blocks/DDHG-ServiceHero/Component.tsx +1 -1
- package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
- package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
- package/templates/blocks/DDHG-TeamCards/Component.tsx +169 -131
|
@@ -94,7 +94,7 @@ export const DDTeam: React.FC<DDTeamProps> = ({
|
|
|
94
94
|
|
|
95
95
|
<div id={sectionId ?? 'team'} className="py-24">
|
|
96
96
|
<div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
|
|
97
|
-
{/* Header */}
|
|
97
|
+
{/* Header */}#BlockName-DDTeam
|
|
98
98
|
<div className="text-center max-w-2xl mx-auto mb-16">
|
|
99
99
|
{eyebrow && (
|
|
100
100
|
<p className="text-[11.5px] font-semibold text-stone-400 tracking-widest uppercase mb-3 top-caption">
|
|
@@ -7,16 +7,14 @@ export type DDTechStackProps = Record<string, any>
|
|
|
7
7
|
|
|
8
8
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
9
9
|
type MediaObj = { url?: string | null; alt?: string | null }
|
|
10
|
-
type TechStackItem = {
|
|
11
|
-
image?: string | MediaObj | null
|
|
12
|
-
icon?: string | null
|
|
13
|
-
iconName?: string | null
|
|
10
|
+
type TechStackItem = {
|
|
11
|
+
image?: string | MediaObj | null
|
|
12
|
+
icon?: string | null
|
|
14
13
|
label?: string | null
|
|
15
14
|
showLabel?: boolean | null
|
|
16
15
|
}
|
|
17
|
-
type TechStackRow = {
|
|
18
|
-
duration?: number | null
|
|
19
|
-
speed?: number | null
|
|
16
|
+
type TechStackRow = {
|
|
17
|
+
duration?: number | null
|
|
20
18
|
direction?: 'left' | 'right' | null
|
|
21
19
|
items?: TechStackItem[] | null
|
|
22
20
|
}
|
|
@@ -34,7 +32,7 @@ function getMediaAlt(media: string | MediaObj | null | undefined): string | null
|
|
|
34
32
|
|
|
35
33
|
// ─── Dynamic Lucide Icon ──────────────────────────────────────────────────────
|
|
36
34
|
function isLocalImageSrc(src?: string | null): src is string {
|
|
37
|
-
return typeof src === 'string' &&
|
|
35
|
+
return typeof src === 'string' && src.startsWith('/api/media/file/')
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
function DynamicIcon({
|
|
@@ -66,7 +64,7 @@ function TechItem({
|
|
|
66
64
|
|
|
67
65
|
return (
|
|
68
66
|
<div className="relative group/tech text-center shrink-0">
|
|
69
|
-
<div className="w-20 h-20 bg-linear-to-b from-[#1a1a1f] to-[#0f0f12] rounded-2xl p-4 transition-all duration-300 hover:scale-105 hover:shadow-[0_0_30px_rgba(140,138,132,0.15)] flex items-center justify-center border border-[#2A2A30] ">
|
|
67
|
+
<div className="w-20 h-20 bg-linear-to-b from-[#1a1a1f] to-[#0f0f12] rounded-2xl p-4 transition-all duration-300 hover:scale-105 hover:shadow-[0_0_30px_rgba(140,138,132,0.15)] flex items-center justify-center border border-[#2A2A30] invert-[1] dark:invert-[0]">
|
|
70
68
|
{safeImageUrl ? (
|
|
71
69
|
<Image
|
|
72
70
|
src={safeImageUrl}
|
|
@@ -88,18 +86,14 @@ function TechItem({
|
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
91
|
-
export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
92
|
-
eyebrow,
|
|
93
|
-
heading,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
customCSS,
|
|
100
|
-
}) => {
|
|
101
|
-
const resolvedHeading = heading ?? [headlineMain, headlineAccent].filter(Boolean).join(' ')
|
|
102
|
-
const rows = (techRows ?? legacyRows ?? []) as TechStackRow[]
|
|
89
|
+
export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
90
|
+
eyebrow,
|
|
91
|
+
heading,
|
|
92
|
+
techRows,
|
|
93
|
+
sectionId,
|
|
94
|
+
customCSS,
|
|
95
|
+
}) => {
|
|
96
|
+
const rows = (techRows ?? []) as TechStackRow[]
|
|
103
97
|
if (!rows.length) return null
|
|
104
98
|
|
|
105
99
|
const rowMaskStyle: React.CSSProperties = {
|
|
@@ -111,22 +105,22 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
111
105
|
return (
|
|
112
106
|
<section
|
|
113
107
|
id={sectionId ?? undefined}
|
|
114
|
-
className="relative
|
|
108
|
+
className="relative w-full bg-transparent py-12 px-4 md:px-6 max-w-6xl mx-auto"
|
|
115
109
|
>
|
|
116
110
|
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
117
111
|
|
|
118
|
-
{(eyebrow ||
|
|
112
|
+
{(eyebrow || heading) && (
|
|
119
113
|
<div className="mb-12">
|
|
120
|
-
|
|
114
|
+
#BlockName-DDTechStack
|
|
121
115
|
{eyebrow && (
|
|
122
116
|
<p className="text-sm uppercase tracking-widest text-[#9A9790] mb-2">{eyebrow}</p>
|
|
123
117
|
)}
|
|
124
|
-
{
|
|
118
|
+
{heading && (
|
|
125
119
|
<h2
|
|
126
120
|
className="text-3xl font-bold tracking-tighter sm:text-4xl text-stone-900 dark:text-stone-50"
|
|
127
121
|
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
|
|
128
122
|
>
|
|
129
|
-
{
|
|
123
|
+
{heading}
|
|
130
124
|
</h2>
|
|
131
125
|
)}
|
|
132
126
|
</div>
|
|
@@ -134,7 +128,7 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
134
128
|
|
|
135
129
|
<div className="relative">
|
|
136
130
|
{rows.map((row, rowIndex) => {
|
|
137
|
-
const duration = row.duration ??
|
|
131
|
+
const duration = row.duration ?? 40
|
|
138
132
|
const direction = row.direction ?? 'left'
|
|
139
133
|
const items = row.items ?? []
|
|
140
134
|
|
|
@@ -164,7 +158,7 @@ export const DDTechStack: React.FC<DDTechStackProps> = ({
|
|
|
164
158
|
key={idx}
|
|
165
159
|
imageUrl={imgUrl}
|
|
166
160
|
imageAlt={imgAlt}
|
|
167
|
-
icon={item.icon
|
|
161
|
+
icon={item.icon}
|
|
168
162
|
label={item.label}
|
|
169
163
|
showLabel={item.showLabel}
|
|
170
164
|
/>
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
2
|
|
|
3
|
-
import React from
|
|
4
|
-
import Image from
|
|
5
|
-
import { Star } from
|
|
6
|
-
import { motion, type Variants } from
|
|
7
|
-
export type DDTestimonialsProps = Record<string, any
|
|
8
|
-
type Media = { url?: string | null }
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
import { Star } from 'lucide-react'
|
|
6
|
+
import { motion, type Variants } from 'framer-motion'
|
|
7
|
+
export type DDTestimonialsProps = Record<string, any>
|
|
8
|
+
type Media = { url?: string | null }
|
|
9
9
|
type Testimonial = {
|
|
10
|
-
id?: string | null
|
|
11
|
-
title?: string | null
|
|
12
|
-
content?: unknown
|
|
13
|
-
designation?: unknown
|
|
14
|
-
companyLogo?: Media | string | null
|
|
15
|
-
fallbackAvatarUrl?: string | null
|
|
16
|
-
rating?: number | null
|
|
17
|
-
}
|
|
10
|
+
id?: string | null
|
|
11
|
+
title?: string | null
|
|
12
|
+
content?: unknown
|
|
13
|
+
designation?: unknown
|
|
14
|
+
companyLogo?: Media | string | null
|
|
15
|
+
fallbackAvatarUrl?: string | null
|
|
16
|
+
rating?: number | null
|
|
17
|
+
}
|
|
18
18
|
|
|
19
19
|
function isLocalImageSrc(src?: string | null): src is string {
|
|
20
|
-
return typeof src ===
|
|
20
|
+
return typeof src === 'string' && src.startsWith('/api/media/file/')
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const containerVariants: Variants = {
|
|
24
24
|
hidden: {},
|
|
25
25
|
show: { transition: { staggerChildren: 0.12, delayChildren: 0.05 } },
|
|
26
|
-
}
|
|
26
|
+
}
|
|
27
27
|
|
|
28
28
|
const cardVariants: Variants = {
|
|
29
29
|
hidden: { opacity: 0, y: 24 },
|
|
30
|
-
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease:
|
|
31
|
-
}
|
|
30
|
+
show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: 'easeOut' } },
|
|
31
|
+
}
|
|
32
32
|
|
|
33
33
|
export const DDTestimonials: React.FC<DDTestimonialsProps> = ({
|
|
34
34
|
eyebrow,
|
|
@@ -38,16 +38,16 @@ export const DDTestimonials: React.FC<DDTestimonialsProps> = ({
|
|
|
38
38
|
customCSS,
|
|
39
39
|
}) => {
|
|
40
40
|
const populated = ((testimonials ?? []) as unknown[]).filter(
|
|
41
|
-
(t): t is Testimonial => typeof t ===
|
|
42
|
-
)
|
|
41
|
+
(t): t is Testimonial => typeof t === 'object' && t !== null,
|
|
42
|
+
)
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<>
|
|
46
46
|
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
47
47
|
|
|
48
|
-
<div id={sectionId ??
|
|
48
|
+
<div id={sectionId ?? 'testimonials'} className="py-24">
|
|
49
49
|
<div className="max-w-6xl lg:max-w-7xl mx-auto px-4 md:px-6">
|
|
50
|
-
{/* Header */}
|
|
50
|
+
{/* Header */}#BlockName-DDTestimonials
|
|
51
51
|
<div className="text-center max-w-xl mx-auto mb-14">
|
|
52
52
|
{eyebrow && (
|
|
53
53
|
<p className="text-[11.5px] font-semibold text-stone-400 tracking-widest uppercase mb-3 top-caption">
|
|
@@ -69,33 +69,26 @@ export const DDTestimonials: React.FC<DDTestimonialsProps> = ({
|
|
|
69
69
|
>
|
|
70
70
|
{populated.map((t, i) => {
|
|
71
71
|
const avatarUrl =
|
|
72
|
-
typeof t.companyLogo ===
|
|
72
|
+
typeof t.companyLogo === 'object' &&
|
|
73
73
|
t.companyLogo !== null &&
|
|
74
|
-
|
|
74
|
+
'url' in t.companyLogo
|
|
75
75
|
? t.companyLogo.url
|
|
76
|
-
: (t.fallbackAvatarUrl ?? null)
|
|
77
|
-
const safeAvatarUrl = isLocalImageSrc(avatarUrl)
|
|
78
|
-
? avatarUrl
|
|
79
|
-
: null;
|
|
76
|
+
: (t.fallbackAvatarUrl ?? null)
|
|
77
|
+
const safeAvatarUrl = isLocalImageSrc(avatarUrl) ? avatarUrl : null
|
|
80
78
|
|
|
81
|
-
const stars = Math.min(5, Math.max(1, t.rating ?? 5))
|
|
79
|
+
const stars = Math.min(5, Math.max(1, t.rating ?? 5))
|
|
82
80
|
|
|
83
81
|
return (
|
|
84
82
|
<motion.div
|
|
85
83
|
key={t.id ?? i}
|
|
86
84
|
className="rounded-2xl bg-stone-100 dark:bg-stone-900 border border-stone-100 dark:border-stone-800 p-8 flex flex-col"
|
|
87
85
|
variants={cardVariants}
|
|
88
|
-
style={{ cornerShape:
|
|
86
|
+
style={{ cornerShape: 'squircle' } as React.CSSProperties}
|
|
89
87
|
>
|
|
90
88
|
{/* Stars */}
|
|
91
89
|
<div className="flex items-center gap-1 mb-5">
|
|
92
90
|
{Array.from({ length: stars }).map((_, si) => (
|
|
93
|
-
<Star
|
|
94
|
-
key={si}
|
|
95
|
-
size={14}
|
|
96
|
-
className="text-amber-400"
|
|
97
|
-
fill="currentColor"
|
|
98
|
-
/>
|
|
91
|
+
<Star key={si} size={14} className="text-amber-400" fill="currentColor" />
|
|
99
92
|
))}
|
|
100
93
|
</div>
|
|
101
94
|
|
|
@@ -104,8 +97,7 @@ export const DDTestimonials: React.FC<DDTestimonialsProps> = ({
|
|
|
104
97
|
<div className="text-sm text-stone-400 leading-relaxed flex-1 mb-6">
|
|
105
98
|
{/* TODO: Reconnect this project's RichText renderer for testimonial content. */}
|
|
106
99
|
<span>
|
|
107
|
-
Reconnect this project's RichText renderer for
|
|
108
|
-
testimonial content.
|
|
100
|
+
Reconnect this project's RichText renderer for testimonial content.
|
|
109
101
|
</span>
|
|
110
102
|
</div>
|
|
111
103
|
) : null}
|
|
@@ -116,7 +108,7 @@ export const DDTestimonials: React.FC<DDTestimonialsProps> = ({
|
|
|
116
108
|
<div className="w-10 h-10 rounded-full overflow-hidden shrink-0 relative">
|
|
117
109
|
<Image
|
|
118
110
|
src={safeAvatarUrl}
|
|
119
|
-
alt={t.title ??
|
|
111
|
+
alt={t.title ?? ''}
|
|
120
112
|
fill
|
|
121
113
|
sizes="40px"
|
|
122
114
|
className="object-cover object-top"
|
|
@@ -136,12 +128,12 @@ export const DDTestimonials: React.FC<DDTestimonialsProps> = ({
|
|
|
136
128
|
</div>
|
|
137
129
|
</div>
|
|
138
130
|
</motion.div>
|
|
139
|
-
)
|
|
131
|
+
)
|
|
140
132
|
})}
|
|
141
133
|
</motion.div>
|
|
142
134
|
)}
|
|
143
135
|
</div>
|
|
144
136
|
</div>
|
|
145
137
|
</>
|
|
146
|
-
)
|
|
147
|
-
}
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import { motion } from 'framer-motion'
|
|
5
|
-
import { cn } from '@/utilities/ui'
|
|
6
|
-
import { Separator } from '@/components/ui/separator'
|
|
7
|
-
|
|
8
|
-
export interface TextMarqueeBlockProps {
|
|
9
|
-
blockType: 'textMarquee'
|
|
10
|
-
items?: Array<{
|
|
11
|
-
text: string
|
|
12
|
-
textClass?: string
|
|
13
|
-
textStyle?: React.CSSProperties
|
|
14
|
-
fontSize?: string
|
|
15
|
-
fontWeight?: string
|
|
16
|
-
color?: string
|
|
17
|
-
}>
|
|
18
|
-
speed?: number
|
|
19
|
-
spacing?: number
|
|
20
|
-
showTopDivider?: boolean
|
|
21
|
-
showBottomDivider?: boolean
|
|
22
|
-
containerClass?: string
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type TextMarqueeProps = {
|
|
26
|
-
texts: {
|
|
27
|
-
text: string
|
|
28
|
-
textClass?: string
|
|
29
|
-
textStyle?: React.CSSProperties
|
|
30
|
-
fontSize?: string
|
|
31
|
-
fontWeight?: string
|
|
32
|
-
color?: string
|
|
33
|
-
}[]
|
|
34
|
-
speed?: number
|
|
35
|
-
spacing?: number
|
|
36
|
-
showTopDivider?: boolean
|
|
37
|
-
showBottomDivider?: boolean
|
|
38
|
-
containerClass?: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const TextMarqueeComponent: React.FC<TextMarqueeProps> = ({
|
|
42
|
-
texts,
|
|
43
|
-
speed = 20,
|
|
44
|
-
spacing = 48,
|
|
45
|
-
showTopDivider = false,
|
|
46
|
-
showBottomDivider = false,
|
|
47
|
-
containerClass = 'overflow-hidden py-8 bg-transparent'
|
|
48
|
-
}) => {
|
|
49
|
-
if (!texts || texts.length === 0) {
|
|
50
|
-
return null
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Calculate total width - simple calculation, no memoization needed
|
|
54
|
-
const totalWidth = texts.reduce((acc, text) => {
|
|
55
|
-
const textWidth = text.text.length * 12 // ~12px per character
|
|
56
|
-
return acc + textWidth + spacing
|
|
57
|
-
}, 0)
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<>
|
|
61
|
-
{showTopDivider && <Separator gradient={true} className="border-t" />}
|
|
62
|
-
<div className={containerClass}>
|
|
63
|
-
<motion.div
|
|
64
|
-
className="flex items-center whitespace-nowrap"
|
|
65
|
-
animate={{ x: [0, -totalWidth] }}
|
|
66
|
-
transition={{
|
|
67
|
-
repeat: Infinity,
|
|
68
|
-
duration: speed,
|
|
69
|
-
ease: 'linear',
|
|
70
|
-
repeatType: 'loop'
|
|
71
|
-
}}
|
|
72
|
-
style={{ width: `${totalWidth * 2}px` }}
|
|
73
|
-
>
|
|
74
|
-
{/* First set of texts */}
|
|
75
|
-
{texts.map((item, index) => (
|
|
76
|
-
<div
|
|
77
|
-
key={`text-${index}`}
|
|
78
|
-
className="
|
|
79
|
-
style={{ marginRight: `${spacing}px` }}
|
|
80
|
-
>
|
|
81
|
-
<span
|
|
82
|
-
className={cn('text-
|
|
83
|
-
style={{
|
|
84
|
-
fontSize: item.fontSize || '
|
|
85
|
-
fontWeight: item.fontWeight || '
|
|
86
|
-
color: item.color || 'currentColor',
|
|
87
|
-
...item.textStyle
|
|
88
|
-
}}
|
|
89
|
-
>
|
|
90
|
-
{item.text}
|
|
91
|
-
</span>
|
|
92
|
-
</div>
|
|
93
|
-
))}
|
|
94
|
-
|
|
95
|
-
{/* Duplicate set for seamless loop */}
|
|
96
|
-
{texts.map((item, index) => (
|
|
97
|
-
<div
|
|
98
|
-
key={`text-${index}-duplicate`}
|
|
99
|
-
className="
|
|
100
|
-
style={{ marginRight: `${spacing}px` }}
|
|
101
|
-
>
|
|
102
|
-
<span
|
|
103
|
-
className={cn('text-
|
|
104
|
-
style={{
|
|
105
|
-
fontSize: item.fontSize || '
|
|
106
|
-
fontWeight: item.fontWeight || '
|
|
107
|
-
color: item.color || 'currentColor',
|
|
108
|
-
...item.textStyle
|
|
109
|
-
}}
|
|
110
|
-
>
|
|
111
|
-
{item.text}
|
|
112
|
-
</span>
|
|
113
|
-
</div>
|
|
114
|
-
))}
|
|
115
|
-
</motion.div>
|
|
116
|
-
</div>
|
|
117
|
-
{showBottomDivider && <Separator gradient={true} className="border-b" />}
|
|
118
|
-
</>
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Block component that transforms props and renders the TextMarquee
|
|
123
|
-
export const DDTextMarq: React.FC<TextMarqueeBlockProps> = (props) => {
|
|
124
|
-
const {
|
|
125
|
-
items = [],
|
|
126
|
-
speed = 20,
|
|
127
|
-
spacing = 48,
|
|
128
|
-
showTopDivider = false,
|
|
129
|
-
showBottomDivider = false,
|
|
130
|
-
containerClass = 'overflow-hidden py-8 bg-transparent'
|
|
131
|
-
} = props
|
|
132
|
-
|
|
133
|
-
// Transform items to match component expectations, using array index instead of id
|
|
134
|
-
const texts = items.map((item, index) => ({
|
|
135
|
-
text: item.text,
|
|
136
|
-
textClass: item.textClass,
|
|
137
|
-
textStyle: item.textStyle,
|
|
138
|
-
fontSize: item.fontSize,
|
|
139
|
-
fontWeight: item.fontWeight,
|
|
140
|
-
color: item.color
|
|
141
|
-
}))
|
|
142
|
-
|
|
143
|
-
if (texts.length === 0) {
|
|
144
|
-
return null
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<TextMarqueeComponent
|
|
149
|
-
texts={texts}
|
|
150
|
-
speed={speed}
|
|
151
|
-
spacing={spacing}
|
|
152
|
-
showTopDivider={showTopDivider}
|
|
153
|
-
showBottomDivider={showBottomDivider}
|
|
154
|
-
containerClass={containerClass}
|
|
155
|
-
/>
|
|
156
|
-
)
|
|
157
|
-
}
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { motion } from 'framer-motion'
|
|
5
|
+
import { cn } from '@/utilities/ui'
|
|
6
|
+
import { Separator } from '@/components/ui/separator'
|
|
7
|
+
|
|
8
|
+
export interface TextMarqueeBlockProps {
|
|
9
|
+
blockType: 'textMarquee'
|
|
10
|
+
items?: Array<{
|
|
11
|
+
text: string
|
|
12
|
+
textClass?: string
|
|
13
|
+
textStyle?: React.CSSProperties
|
|
14
|
+
fontSize?: string
|
|
15
|
+
fontWeight?: string
|
|
16
|
+
color?: string
|
|
17
|
+
}>
|
|
18
|
+
speed?: number
|
|
19
|
+
spacing?: number
|
|
20
|
+
showTopDivider?: boolean
|
|
21
|
+
showBottomDivider?: boolean
|
|
22
|
+
containerClass?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TextMarqueeProps = {
|
|
26
|
+
texts: {
|
|
27
|
+
text: string
|
|
28
|
+
textClass?: string
|
|
29
|
+
textStyle?: React.CSSProperties
|
|
30
|
+
fontSize?: string
|
|
31
|
+
fontWeight?: string
|
|
32
|
+
color?: string
|
|
33
|
+
}[]
|
|
34
|
+
speed?: number
|
|
35
|
+
spacing?: number
|
|
36
|
+
showTopDivider?: boolean
|
|
37
|
+
showBottomDivider?: boolean
|
|
38
|
+
containerClass?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const TextMarqueeComponent: React.FC<TextMarqueeProps> = ({
|
|
42
|
+
texts,
|
|
43
|
+
speed = 20,
|
|
44
|
+
spacing = 48,
|
|
45
|
+
showTopDivider = false,
|
|
46
|
+
showBottomDivider = false,
|
|
47
|
+
containerClass = 'overflow-hidden py-8 bg-transparent',
|
|
48
|
+
}) => {
|
|
49
|
+
if (!texts || texts.length === 0) {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Calculate total width - simple calculation, no memoization needed
|
|
54
|
+
const totalWidth = texts.reduce((acc, text) => {
|
|
55
|
+
const textWidth = text.text.length * 12 // ~12px per character
|
|
56
|
+
return acc + textWidth + spacing
|
|
57
|
+
}, 0)
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
{showTopDivider && <Separator gradient={true} className="border-t" />}
|
|
62
|
+
<div className={cn('overflow-clip relative top-24', containerClass)}>
|
|
63
|
+
<motion.div
|
|
64
|
+
className="flex items-center whitespace-nowrap"
|
|
65
|
+
animate={{ x: [0, -totalWidth] }}
|
|
66
|
+
transition={{
|
|
67
|
+
repeat: Infinity,
|
|
68
|
+
duration: speed,
|
|
69
|
+
ease: 'linear',
|
|
70
|
+
repeatType: 'loop',
|
|
71
|
+
}}
|
|
72
|
+
style={{ width: `${totalWidth * 2}px` }}
|
|
73
|
+
>
|
|
74
|
+
{/* First set of texts */}
|
|
75
|
+
{texts.map((item, index) => (
|
|
76
|
+
<div
|
|
77
|
+
key={`text-${index}`}
|
|
78
|
+
className="shrink-0 flex items-center justify-center"
|
|
79
|
+
style={{ marginRight: `${spacing}px` }}
|
|
80
|
+
>
|
|
81
|
+
<span
|
|
82
|
+
className={cn('text-center whitespace-nowrap uppercase', item.textClass)}
|
|
83
|
+
style={{
|
|
84
|
+
fontSize: item.fontSize || '4rem',
|
|
85
|
+
fontWeight: item.fontWeight || 'bold',
|
|
86
|
+
color: item.color || 'currentColor',
|
|
87
|
+
...item.textStyle,
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
{item.text}
|
|
91
|
+
</span>
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
|
|
95
|
+
{/* Duplicate set for seamless loop */}
|
|
96
|
+
{texts.map((item, index) => (
|
|
97
|
+
<div
|
|
98
|
+
key={`text-${index}-duplicate`}
|
|
99
|
+
className="shrink-0 flex items-center justify-center"
|
|
100
|
+
style={{ marginRight: `${spacing}px` }}
|
|
101
|
+
>
|
|
102
|
+
<span
|
|
103
|
+
className={cn('text-center whitespace-nowrap uppercase', item.textClass)}
|
|
104
|
+
style={{
|
|
105
|
+
fontSize: item.fontSize || '4rem',
|
|
106
|
+
fontWeight: item.fontWeight || 'bold',
|
|
107
|
+
color: item.color || 'currentColor',
|
|
108
|
+
...item.textStyle,
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{item.text}
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
))}
|
|
115
|
+
</motion.div>
|
|
116
|
+
</div>
|
|
117
|
+
{showBottomDivider && <Separator gradient={true} className="border-b" />}
|
|
118
|
+
</>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Block component that transforms props and renders the TextMarquee
|
|
123
|
+
export const DDTextMarq: React.FC<TextMarqueeBlockProps> = (props) => {
|
|
124
|
+
const {
|
|
125
|
+
items = [],
|
|
126
|
+
speed = 20,
|
|
127
|
+
spacing = 48,
|
|
128
|
+
showTopDivider = false,
|
|
129
|
+
showBottomDivider = false,
|
|
130
|
+
containerClass = 'overflow-hidden py-8 bg-transparent',
|
|
131
|
+
} = props
|
|
132
|
+
|
|
133
|
+
// Transform items to match component expectations, using array index instead of id
|
|
134
|
+
const texts = items.map((item, index) => ({
|
|
135
|
+
text: item.text,
|
|
136
|
+
textClass: item.textClass,
|
|
137
|
+
textStyle: item.textStyle,
|
|
138
|
+
fontSize: item.fontSize,
|
|
139
|
+
fontWeight: item.fontWeight,
|
|
140
|
+
color: item.color,
|
|
141
|
+
}))
|
|
142
|
+
|
|
143
|
+
if (texts.length === 0) {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<TextMarqueeComponent
|
|
149
|
+
texts={texts}
|
|
150
|
+
speed={speed}
|
|
151
|
+
spacing={spacing}
|
|
152
|
+
showTopDivider={showTopDivider}
|
|
153
|
+
showBottomDivider={showBottomDivider}
|
|
154
|
+
containerClass={containerClass}
|
|
155
|
+
/>
|
|
156
|
+
)
|
|
157
|
+
}
|