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
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
RefreshCw,
|
|
12
12
|
ShieldCheck,
|
|
13
13
|
User,
|
|
14
|
-
UserStar
|
|
14
|
+
UserStar,
|
|
15
15
|
} from 'lucide-react'
|
|
16
16
|
import React, { useState } from 'react'
|
|
17
17
|
|
|
@@ -69,33 +69,33 @@ const defaultTrustBadges: TrustBadge[] = [
|
|
|
69
69
|
{
|
|
70
70
|
icon: 'shieldCheck',
|
|
71
71
|
title: 'Transparent Pricing',
|
|
72
|
-
description: 'Every quote is itemized. No hidden fees, no surprise charges at the end.'
|
|
72
|
+
description: 'Every quote is itemized. No hidden fees, no surprise charges at the end.',
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
icon: 'calendarCheck',
|
|
76
76
|
title: 'Fixed Timeline Guarantee',
|
|
77
|
-
description: 'We commit to a delivery date and stick to it. If we miss it, you get a credit.'
|
|
77
|
+
description: 'We commit to a delivery date and stick to it. If we miss it, you get a credit.',
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
icon: 'userStar',
|
|
81
81
|
title: 'Dedicated Project Lead',
|
|
82
|
-
description: 'One senior designer owns your project from kickoff to handover. No handoffs.'
|
|
82
|
+
description: 'One senior designer owns your project from kickoff to handover. No handoffs.',
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
icon: 'fileText',
|
|
86
86
|
title: 'Full Documentation',
|
|
87
|
-
description: 'Every finish, fixture, and specification is documented for future reference.'
|
|
87
|
+
description: 'Every finish, fixture, and specification is documented for future reference.',
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
icon: 'refreshCw',
|
|
91
91
|
title: 'Revision Rounds Included',
|
|
92
|
-
description: 'Three structured revision cycles are included in every engagement.'
|
|
92
|
+
description: 'Three structured revision cycles are included in every engagement.',
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
icon: 'handHeart',
|
|
96
96
|
title: 'Post-Completion Support',
|
|
97
|
-
description: '30 days of complimentary support after handover. We stand by our work.'
|
|
98
|
-
}
|
|
97
|
+
description: '30 days of complimentary support after handover. We stand by our work.',
|
|
98
|
+
},
|
|
99
99
|
]
|
|
100
100
|
|
|
101
101
|
const defaultTestimonials: TestimonialItem[] = [
|
|
@@ -103,42 +103,42 @@ const defaultTestimonials: TestimonialItem[] = [
|
|
|
103
103
|
quote:
|
|
104
104
|
'They delivered exactly what they promised, on the date they promised it, at the price they quoted. That alone is rare in this industry.',
|
|
105
105
|
author: 'Margaret Chen',
|
|
106
|
-
role: 'Director of Operations, Meridian Holdings'
|
|
106
|
+
role: 'Director of Operations, Meridian Holdings',
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
quote:
|
|
110
110
|
'The transparency throughout the process was remarkable. We knew exactly where we stood at every phase. No surprises.',
|
|
111
111
|
author: 'James Oduya',
|
|
112
|
-
role: 'Founder, The Atelier Collective'
|
|
113
|
-
}
|
|
112
|
+
role: 'Founder, The Atelier Collective',
|
|
113
|
+
},
|
|
114
114
|
]
|
|
115
115
|
|
|
116
116
|
const defaultFaqs: FAQItem[] = [
|
|
117
117
|
{
|
|
118
118
|
question: 'How long does a typical project take?',
|
|
119
119
|
answer:
|
|
120
|
-
'Project timelines vary based on scope. A residential interior typically takes 8-16 weeks from concept to completion. Commercial fit-outs range from 12-24 weeks. We provide detailed timelines during our initial consultation.'
|
|
120
|
+
'Project timelines vary based on scope. A residential interior typically takes 8-16 weeks from concept to completion. Commercial fit-outs range from 12-24 weeks. We provide detailed timelines during our initial consultation.',
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
question: 'Do you handle both design and construction?',
|
|
124
124
|
answer:
|
|
125
|
-
'Yes, our Turn-Key Services cover the complete journey from initial concept through construction to final handover. We coordinate all contractors and trades, ensuring the design vision is executed faithfully.'
|
|
125
|
+
'Yes, our Turn-Key Services cover the complete journey from initial concept through construction to final handover. We coordinate all contractors and trades, ensuring the design vision is executed faithfully.',
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
128
|
question: 'Can you work with existing furniture and art?',
|
|
129
129
|
answer:
|
|
130
|
-
'Absolutely. Our Atmospheric Curation service specializes in integrating existing pieces into new designs. We view heirlooms and collected art as anchors of identity within the space.'
|
|
130
|
+
'Absolutely. Our Atmospheric Curation service specializes in integrating existing pieces into new designs. We view heirlooms and collected art as anchors of identity within the space.',
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
question: 'What is your fee structure?',
|
|
134
134
|
answer:
|
|
135
|
-
'We offer both fixed-fee and percentage-based pricing depending on project type. Design Consultancy is typically fixed-fee, while Turn-Key projects use a percentage of construction cost. We provide transparent quotes upfront.'
|
|
135
|
+
'We offer both fixed-fee and percentage-based pricing depending on project type. Design Consultancy is typically fixed-fee, while Turn-Key projects use a percentage of construction cost. We provide transparent quotes upfront.',
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
question: 'Do you offer sustainability-focused design?',
|
|
139
139
|
answer:
|
|
140
|
-
'Sustainability is woven into our material selection and spatial planning. We prioritize low-VOC finishes, responsibly sourced materials, energy-efficient lighting, and biophilic elements that reduce environmental impact.'
|
|
141
|
-
}
|
|
140
|
+
'Sustainability is woven into our material selection and spatial planning. We prioritize low-VOC finishes, responsibly sourced materials, energy-efficient lighting, and biophilic elements that reduce environmental impact.',
|
|
141
|
+
},
|
|
142
142
|
]
|
|
143
143
|
|
|
144
144
|
const iconMap = {
|
|
@@ -148,13 +148,13 @@ const iconMap = {
|
|
|
148
148
|
fileText: FileText,
|
|
149
149
|
refreshCw: RefreshCw,
|
|
150
150
|
handHeart: HandHeart,
|
|
151
|
-
clipboardList: ClipboardList
|
|
151
|
+
clipboardList: ClipboardList,
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function RevealBlock({
|
|
155
155
|
children,
|
|
156
156
|
className,
|
|
157
|
-
delay = 0
|
|
157
|
+
delay = 0,
|
|
158
158
|
}: {
|
|
159
159
|
children: React.ReactNode
|
|
160
160
|
className?: string
|
|
@@ -190,7 +190,7 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
190
190
|
faqEyebrowEmphasis = 'questions',
|
|
191
191
|
faqHeading = 'FAQ',
|
|
192
192
|
showFaqDivider = true,
|
|
193
|
-
faqs
|
|
193
|
+
faqs,
|
|
194
194
|
}) => {
|
|
195
195
|
const [openFAQ, setOpenFAQ] = useState<number | null>(null)
|
|
196
196
|
const testimonialItems = getItems(testimonials, defaultTestimonials)
|
|
@@ -213,29 +213,29 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
213
213
|
{testimonialItems.map((testimonial, i) => (
|
|
214
214
|
<div
|
|
215
215
|
key={testimonial.id || `testimonial-${i}`}
|
|
216
|
-
className="z-plane-1 chamfer-accent p-6 md:p-8"
|
|
216
|
+
className="z-plane-1 chamfer-accent rounded-md squircle border border-stone-300 bg-stone-100/50 p-6 transition-all duration-300 dark:border-stone-700/10 dark:bg-stone-900/50 md:p-8"
|
|
217
217
|
>
|
|
218
218
|
<div className="mb-4 h-px w-8 bg-[#C4A882]" />
|
|
219
219
|
{testimonial.quote && (
|
|
220
|
-
<p className="mb-6 text-
|
|
220
|
+
<p className="mb-6 text-base font-light leading-relaxed text-stone-700 dark:text-stone-200">
|
|
221
221
|
“{testimonial.quote}”
|
|
222
222
|
</p>
|
|
223
223
|
)}
|
|
224
224
|
<div className="flex items-center gap-3">
|
|
225
225
|
<div
|
|
226
226
|
className="flex h-8 w-8 items-center justify-center bg-stone-500"
|
|
227
|
-
style={{ borderRadius: '
|
|
227
|
+
style={{ borderRadius: '6px' }}
|
|
228
228
|
>
|
|
229
229
|
<User className="h-3.5 w-3.5 text-stone-50" strokeWidth={1.6} />
|
|
230
230
|
</div>
|
|
231
231
|
<div>
|
|
232
232
|
{testimonial.author && (
|
|
233
|
-
<p className="text-
|
|
233
|
+
<p className="text-base font-medium text-stone-700 dark:text-stone-200">
|
|
234
234
|
{testimonial.author}
|
|
235
235
|
</p>
|
|
236
236
|
)}
|
|
237
237
|
{testimonial.role && (
|
|
238
|
-
<p className="text-
|
|
238
|
+
<p className="text-base text-stone-600 dark:text-stone-300">
|
|
239
239
|
{testimonial.role}
|
|
240
240
|
</p>
|
|
241
241
|
)}
|
|
@@ -249,10 +249,10 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
249
249
|
|
|
250
250
|
{badgeItems.length > 0 && (
|
|
251
251
|
<RevealBlock className="whytrustus-section mb-16">
|
|
252
|
-
<div className="z-plane-1 p-6 md:p-8">
|
|
252
|
+
<div className="z-plane-1 rounded-md squircle border border-stone-300 bg-stone-100/50 p-6 dark:border-stone-700/10 dark:bg-stone-900/50 md:p-8">
|
|
253
253
|
<div className="mb-6">
|
|
254
254
|
{commitmentLabel && (
|
|
255
|
-
<p className="mb-2 font-mono text-
|
|
255
|
+
<p className="mb-2 font-mono text-base uppercase tracking-[0.15em] text-stone-600 dark:text-stone-300">
|
|
256
256
|
{commitmentLabel}
|
|
257
257
|
</p>
|
|
258
258
|
)}
|
|
@@ -269,19 +269,19 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
269
269
|
return (
|
|
270
270
|
<div
|
|
271
271
|
key={badge.id || `${badge.title || 'badge'}-${i}`}
|
|
272
|
-
className="arch-edge-sm flex items-start gap-3 border border-
|
|
272
|
+
className="arch-edge-sm flex items-start gap-3 rounded-md squircle border border-stone-300 bg-stone-100/50 p-4 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg dark:border-stone-700/10 dark:bg-stone-900/50"
|
|
273
273
|
>
|
|
274
|
-
<div className="mt-0.5 flex h-6 w-6
|
|
274
|
+
<div className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center">
|
|
275
275
|
<Icon className="h-4 w-4 text-[#C4A882]" strokeWidth={1.7} />
|
|
276
276
|
</div>
|
|
277
277
|
<div>
|
|
278
278
|
{badge.title && (
|
|
279
|
-
<p className="mb-1 text-
|
|
279
|
+
<p className="mb-1 text-base font-medium text-stone-700 dark:text-stone-200">
|
|
280
280
|
{badge.title}
|
|
281
281
|
</p>
|
|
282
282
|
)}
|
|
283
283
|
{description && (
|
|
284
|
-
<p className="text-
|
|
284
|
+
<p className="text-xs font-light leading-relaxed text-stone-700 dark:text-stone-200">
|
|
285
285
|
{description}
|
|
286
286
|
</p>
|
|
287
287
|
)}
|
|
@@ -296,7 +296,7 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
296
296
|
|
|
297
297
|
<RevealBlock className="faq-section max-w-4xl mx-auto mb-10">
|
|
298
298
|
{(faqEyebrowPrefix || faqEyebrowEmphasis) && (
|
|
299
|
-
<p className="mb-2 text-
|
|
299
|
+
<p className="mb-2 text-xs uppercase tracking-[0.2em] text-stone-600 dark:text-stone-300">
|
|
300
300
|
{faqEyebrowPrefix && <span className="font-light">{faqEyebrowPrefix} </span>}
|
|
301
301
|
{faqEyebrowEmphasis && <span className="font-medium">{faqEyebrowEmphasis}</span>}
|
|
302
302
|
</p>
|
|
@@ -320,9 +320,9 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
320
320
|
<RevealBlock key={faq.id || `${question || 'faq'}-${i}`} delay={i * 0.06}>
|
|
321
321
|
<div
|
|
322
322
|
className={cn(
|
|
323
|
-
'accordion-card z-plane-1 border-border border chamfer-accent transition-all duration-300',
|
|
323
|
+
'accordion-card z-plane-1 rounded-md squircle border-border border chamfer-accent bg-stone-100/50 transition-all duration-300 dark:bg-stone-900/50',
|
|
324
324
|
isOpen &&
|
|
325
|
-
'border-border border z-depth-hover hover:shadow-2xl hover:-translate-y-2'
|
|
325
|
+
'border-border border z-depth-hover hover:shadow-2xl hover:-translate-y-2',
|
|
326
326
|
)}
|
|
327
327
|
>
|
|
328
328
|
<button
|
|
@@ -332,7 +332,7 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
332
332
|
aria-expanded={isOpen}
|
|
333
333
|
>
|
|
334
334
|
<div className="flex items-center gap-4">
|
|
335
|
-
<span className="font-mono text-
|
|
335
|
+
<span className="font-mono text-base text-stone-600 dark:text-stone-300">
|
|
336
336
|
{String(i + 1).padStart(2, '0')}
|
|
337
337
|
</span>
|
|
338
338
|
{question && (
|
|
@@ -343,15 +343,15 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
343
343
|
</div>
|
|
344
344
|
<div
|
|
345
345
|
className={cn(
|
|
346
|
-
'ml-4 flex h-6 w-6
|
|
347
|
-
isOpen && 'border-[#292524] bg-[#292524]'
|
|
346
|
+
'ml-4 flex h-6 w-6 shrink-0 items-center justify-center rounded-md squircle border border-stone-300 dark:border-stone-700/10 transition-all duration-300',
|
|
347
|
+
isOpen && 'border-[#292524] bg-[#292524]',
|
|
348
348
|
)}
|
|
349
|
-
style={{ borderRadius: '
|
|
349
|
+
style={{ borderRadius: '6px' }}
|
|
350
350
|
>
|
|
351
351
|
<ToggleIcon
|
|
352
352
|
className={cn(
|
|
353
353
|
'h-3 w-3',
|
|
354
|
-
isOpen ? 'text-[#F5F3EF]' : 'text-stone-600 dark:text-stone-300'
|
|
354
|
+
isOpen ? 'text-[#F5F3EF]' : 'text-stone-600 dark:text-stone-300',
|
|
355
355
|
)}
|
|
356
356
|
strokeWidth={1.8}
|
|
357
357
|
/>
|
|
@@ -368,7 +368,7 @@ export const DDHGStatsFAQ: React.FC<DDHGStatsFAQProps> = ({
|
|
|
368
368
|
>
|
|
369
369
|
<div className="px-5 pb-5 pl-14 md:px-6 md:pb-6 md:pl-16">
|
|
370
370
|
<div className="mb-4 h-px w-full bg-stone-500" />
|
|
371
|
-
<p className="text-
|
|
371
|
+
<p className="text-base font-light leading-relaxed text-stone-700 dark:text-stone-200">
|
|
372
372
|
{answer}
|
|
373
373
|
</p>
|
|
374
374
|
</div>
|
|
@@ -1,131 +1,169 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef } from 'react'
|
|
4
|
-
import Image from 'next/image'
|
|
5
|
-
import Link from 'next/link'
|
|
6
|
-
import { Globe, Briefcase, Camera } from 'lucide-react'
|
|
7
|
-
import { cn } from '@/utilities/ui'
|
|
8
|
-
import { getMediaUrl } from '@/utilities/getMediaUrl'
|
|
9
|
-
|
|
10
|
-
type MediaDoc = {
|
|
11
|
-
url?: string | null
|
|
12
|
-
alt?: string | null
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type TeamMember = {
|
|
16
|
-
name?: string | null
|
|
17
|
-
role?: string | null
|
|
18
|
-
bio?: string | null
|
|
19
|
-
image?: number | MediaDoc | null
|
|
20
|
-
priority?: boolean | null
|
|
21
|
-
facebookUrl?: string | null
|
|
22
|
-
linkedinUrl?: string | null
|
|
23
|
-
instagramUrl?: string | null
|
|
24
|
-
id?: string | null
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type DDHGTeamCardsProps = {
|
|
28
|
-
sectionId?: string | null
|
|
29
|
-
customClassName?: string | null
|
|
30
|
-
customCSS?: string | null
|
|
31
|
-
members?: TeamMember[] | null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getMediaDoc(img?: number | MediaDoc | null): MediaDoc | null {
|
|
35
|
-
if (!img || typeof img === 'number') return null
|
|
36
|
-
return img
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const DDHGTeamCards: React.FC<DDHGTeamCardsProps> = ({
|
|
40
|
-
sectionId,
|
|
41
|
-
customClassName,
|
|
42
|
-
customCSS,
|
|
43
|
-
members,
|
|
44
|
-
}) => {
|
|
45
|
-
const ref = useRef<HTMLDivElement>(null)
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
const observer = new IntersectionObserver(
|
|
49
|
-
(entries) => {
|
|
50
|
-
entries.forEach((entry) => {
|
|
51
|
-
if (entry.isIntersecting) {
|
|
52
|
-
entry.target.querySelectorAll<HTMLElement>('.reveal').forEach((el, i) => {
|
|
53
|
-
setTimeout(() => {
|
|
54
|
-
el.style.opacity = '1'
|
|
55
|
-
el.style.transform = 'translateY(0)'
|
|
56
|
-
}, i * 100)
|
|
57
|
-
})
|
|
58
|
-
observer.unobserve(entry.target)
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
},
|
|
62
|
-
{ threshold: 0.1 },
|
|
63
|
-
)
|
|
64
|
-
if (ref.current) observer.observe(ref.current)
|
|
65
|
-
return () => observer.disconnect()
|
|
66
|
-
}, [])
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div id={sectionId ?? undefined} className={cn('px-5 md:px-8 py-8', customClassName)} ref={ref}>
|
|
70
|
-
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
71
|
-
<div className="max-w-7xl mx-auto">
|
|
72
|
-
{members && members.length > 0 && (
|
|
73
|
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 md:gap-6">
|
|
74
|
-
{members.map((member, i) => {
|
|
75
|
-
const imageDoc = getMediaDoc(member.image)
|
|
76
|
-
const imageUrl = getMediaUrl(imageDoc?.url)
|
|
77
|
-
const socials = [
|
|
78
|
-
{ Icon: Globe, url: member.facebookUrl, label: 'Facebook' },
|
|
79
|
-
{ Icon: Briefcase, url: member.linkedinUrl, label: 'LinkedIn' },
|
|
80
|
-
{ Icon: Camera, url: member.instagramUrl, label: 'Instagram' },
|
|
81
|
-
].filter((s) => !!s.url)
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<div
|
|
85
|
-
key={member.id != null ? `id-${member.id}` : `idx-${i}`}
|
|
86
|
-
className="reveal group relative overflow-hidden cursor-pointer bg-
|
|
87
|
-
style={{
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
import Image from 'next/image'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { Globe, Briefcase, Camera } from 'lucide-react'
|
|
7
|
+
import { cn } from '@/utilities/ui'
|
|
8
|
+
import { getMediaUrl } from '@/utilities/getMediaUrl'
|
|
9
|
+
|
|
10
|
+
type MediaDoc = {
|
|
11
|
+
url?: string | null
|
|
12
|
+
alt?: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type TeamMember = {
|
|
16
|
+
name?: string | null
|
|
17
|
+
role?: string | null
|
|
18
|
+
bio?: string | null
|
|
19
|
+
image?: number | MediaDoc | null
|
|
20
|
+
priority?: boolean | null
|
|
21
|
+
facebookUrl?: string | null
|
|
22
|
+
linkedinUrl?: string | null
|
|
23
|
+
instagramUrl?: string | null
|
|
24
|
+
id?: string | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type DDHGTeamCardsProps = {
|
|
28
|
+
sectionId?: string | null
|
|
29
|
+
customClassName?: string | null
|
|
30
|
+
customCSS?: string | null
|
|
31
|
+
members?: TeamMember[] | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getMediaDoc(img?: number | MediaDoc | null): MediaDoc | null {
|
|
35
|
+
if (!img || typeof img === 'number') return null
|
|
36
|
+
return img
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DDHGTeamCards: React.FC<DDHGTeamCardsProps> = ({
|
|
40
|
+
sectionId,
|
|
41
|
+
customClassName,
|
|
42
|
+
customCSS,
|
|
43
|
+
members,
|
|
44
|
+
}) => {
|
|
45
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const observer = new IntersectionObserver(
|
|
49
|
+
(entries) => {
|
|
50
|
+
entries.forEach((entry) => {
|
|
51
|
+
if (entry.isIntersecting) {
|
|
52
|
+
entry.target.querySelectorAll<HTMLElement>('.reveal').forEach((el, i) => {
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
el.style.opacity = '1'
|
|
55
|
+
el.style.transform = 'translateY(0)'
|
|
56
|
+
}, i * 100)
|
|
57
|
+
})
|
|
58
|
+
observer.unobserve(entry.target)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
{ threshold: 0.1 },
|
|
63
|
+
)
|
|
64
|
+
if (ref.current) observer.observe(ref.current)
|
|
65
|
+
return () => observer.disconnect()
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div id={sectionId ?? undefined} className={cn('px-5 md:px-8 py-8', customClassName)} ref={ref}>
|
|
70
|
+
{customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
|
|
71
|
+
<div className="max-w-7xl mx-auto">
|
|
72
|
+
{members && members.length > 0 && (
|
|
73
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 md:gap-6">
|
|
74
|
+
{members.map((member, i) => {
|
|
75
|
+
const imageDoc = getMediaDoc(member.image)
|
|
76
|
+
const imageUrl = getMediaUrl(imageDoc?.url)
|
|
77
|
+
const socials = [
|
|
78
|
+
{ Icon: Globe, url: member.facebookUrl, label: 'Facebook' },
|
|
79
|
+
{ Icon: Briefcase, url: member.linkedinUrl, label: 'LinkedIn' },
|
|
80
|
+
{ Icon: Camera, url: member.instagramUrl, label: 'Instagram' },
|
|
81
|
+
].filter((s) => !!s.url)
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
key={member.id != null ? `id-${member.id}` : `idx-${i}`}
|
|
86
|
+
className="reveal group relative overflow-hidden cursor-pointer rounded-md squircle border-none border-stone-300/0 bg-stone-100/50 transition-all duration-300 hover:shadow-2xl dark:border-stone-700/0 dark:bg-stone-900/50 h-112 md:h-128"
|
|
87
|
+
style={{
|
|
88
|
+
opacity: 0,
|
|
89
|
+
transform: 'translateY(20px)',
|
|
90
|
+
transition: `opacity 0.9s ease ${i * 0.1}s, transform 0.9s ease ${i * 0.1}s`,
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
{imageDoc && imageUrl && (
|
|
94
|
+
<div className="relative w-full h-full overflow-hidden brightness-100 group-hover:brightness-50 transition-all duration-300">
|
|
95
|
+
<Image
|
|
96
|
+
src={imageUrl}
|
|
97
|
+
alt={imageDoc.alt ?? member.name ?? ''}
|
|
98
|
+
fill
|
|
99
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
100
|
+
priority={Boolean(member.priority)}
|
|
101
|
+
className="team-member object-cover object-top brightness-100 group-hover:brightness-50 group-hover:scale-110 transition-transform duration-500 ease-out"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
<div
|
|
106
|
+
className="pattern-overlay absolute inset-0 opacity-[0.03] pointer-events-none"
|
|
107
|
+
style={{
|
|
108
|
+
backgroundImage:
|
|
109
|
+
'repeating-linear-gradient(90deg, #000 0px, #000 1px, transparent 1px, transparent 20px)',
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
<div className="gradient-overlay absolute inset-0 bg-linear-to-t from-black/80 via-black/80 to-transparent group-hover:from-black/90 transition-all duration-300 group-hover:backdrop-brightness-50" />
|
|
113
|
+
<div className="content-card absolute inset-0 flex flex-col justify-end p-3 text-white">
|
|
114
|
+
<div className="z-10 text-center mb-3">
|
|
115
|
+
{member.name && (
|
|
116
|
+
<h3
|
|
117
|
+
className="text-xl md:text-2xl font-bold mb-1 text-white drop-shadow-lg"
|
|
118
|
+
style={{ fontFamily: 'Outfit, sans-serif', fontWeight: 700 }}
|
|
119
|
+
>
|
|
120
|
+
{member.name}
|
|
121
|
+
</h3>
|
|
122
|
+
)}
|
|
123
|
+
{member.role && (
|
|
124
|
+
<p
|
|
125
|
+
className="text-xs md:text-sm font-medium uppercase tracking-wider text-stone-200 dark:text-stone-200 drop-shadow-md"
|
|
126
|
+
style={{ fontFamily: "'Inter', sans-serif", fontWeight: 500 }}
|
|
127
|
+
>
|
|
128
|
+
{member.role}
|
|
129
|
+
</p>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
<div
|
|
133
|
+
className="hover-panel transform translate-y-full group-hover:translate-y-0 transition-transform duration-300 ease-out p-4"
|
|
134
|
+
style={{ background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(8px)' }}
|
|
135
|
+
>
|
|
136
|
+
{member.bio && (
|
|
137
|
+
<p className="text-sm text-white/70 leading-relaxed mb-4 opacity-0 group-hover:opacity-100 transition-all duration-300 delay-100">
|
|
138
|
+
{member.bio}
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
141
|
+
{socials.length > 0 && (
|
|
142
|
+
<div className="socials-map flex gap-3 justify-center opacity-0 group-hover:opacity-100 transition-all duration-300 delay-200">
|
|
143
|
+
{socials.map(({ Icon, url, label }) => (
|
|
144
|
+
<Link
|
|
145
|
+
key={label}
|
|
146
|
+
href={url!}
|
|
147
|
+
target="_blank"
|
|
148
|
+
rel="noopener noreferrer"
|
|
149
|
+
aria-label={`${member.name} on ${label}`}
|
|
150
|
+
className="text-white/70 hover:text-white hover:scale-110 transition-all duration-200 p-2 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm"
|
|
151
|
+
>
|
|
152
|
+
<Icon size={14} />
|
|
153
|
+
</Link>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)
|
|
161
|
+
})}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export default DDHGTeamCards
|