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.
Files changed (46) hide show
  1. package/package.json +5 -2
  2. package/src/cli.js +39 -3
  3. package/src/commands/shop.js +59 -0
  4. package/src/commands/warehouse.js +73 -0
  5. package/templates/blocks/DD-BookCall/Component.tsx +535 -534
  6. package/templates/blocks/DD-CardTemplate01/Component.tsx +140 -139
  7. package/templates/blocks/DD-Carousel-A/Component.tsx +1 -1
  8. package/templates/blocks/DD-Carousel-B/Component.tsx +1 -1
  9. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  10. package/templates/blocks/DD-ComparisonTable/Component.tsx +1 -1
  11. package/templates/blocks/DD-Contact/Component.tsx +8 -7
  12. package/templates/blocks/DD-Counter/Component.tsx +10 -10
  13. package/templates/blocks/DD-FeatCat/Component.tsx +1 -1
  14. package/templates/blocks/DD-FeatStrip/Component.tsx +1 -1
  15. package/templates/blocks/DD-Hero/Component.tsx +297 -320
  16. package/templates/blocks/DD-HorizontalScroll/Component.tsx +936 -938
  17. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  18. package/templates/blocks/DD-MasonaryMedia/Component.tsx +8 -10
  19. package/templates/blocks/DD-Pricing/Component.tsx +369 -372
  20. package/templates/blocks/DD-Process/Component.tsx +147 -149
  21. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  22. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  23. package/templates/blocks/DD-Section/Component.tsx +1 -1
  24. package/templates/blocks/DD-Services/Component.tsx +1 -1
  25. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +13 -13
  26. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  27. package/templates/blocks/DD-StackCards/Component.tsx +473 -473
  28. package/templates/blocks/DD-Team/Component.tsx +1 -1
  29. package/templates/blocks/DD-TechStack/Component.tsx +22 -28
  30. package/templates/blocks/DD-Testimonials/Component.tsx +35 -43
  31. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  32. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  33. package/templates/blocks/DD-Work/Component.tsx +4 -4
  34. package/templates/blocks/DD-Work-B/Component.tsx +1 -1
  35. package/templates/blocks/DDHG-CTASection2/Component.tsx +6 -6
  36. package/templates/blocks/DDHG-HighlightArc/Component.tsx +243 -243
  37. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +143 -93
  38. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  39. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +227 -213
  40. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  41. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  42. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +24 -28
  43. package/templates/blocks/DDHG-ServiceHero/Component.tsx +1 -1
  44. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  45. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  46. 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-[13px] font-light leading-relaxed text-stone-700 dark:text-stone-200">
220
+ <p className="mb-6 text-base font-light leading-relaxed text-stone-700 dark:text-stone-200">
221
221
  &ldquo;{testimonial.quote}&rdquo;
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: '2px' }}
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-[13px] font-medium text-stone-700 dark:text-stone-200">
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-[13px] text-stone-600 dark:text-stone-300">
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-[13px] uppercase tracking-[0.15em] text-stone-600 dark:text-stone-300">
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-[#D9D7D1] p-4"
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 flex-shrink-0 items-center justify-center">
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-[13px] font-medium text-stone-700 dark:text-stone-200">
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-[10px] font-light leading-relaxed text-stone-700 dark:text-stone-200">
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-[10px] uppercase tracking-[0.2em] text-stone-600 dark:text-stone-300">
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-[13px] text-stone-600 dark:text-stone-300">
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 flex-shrink-0 items-center justify-center border border-[#D9D7D1] transition-all duration-300',
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: '2px' }}
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-[13px] font-light leading-relaxed text-stone-700 dark:text-stone-200">
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-transparent h-[28rem] md:h-[32rem]"
87
- style={{ opacity: 0, transform: 'translateY(20px)', transition: `opacity 0.9s ease ${i * 0.1}s, transform 0.9s ease ${i * 0.1}s` }}
88
- >
89
- {imageDoc && imageUrl && (
90
- <div className="relative w-full h-full overflow-hidden brightness-100 group-hover:brightness-50">
91
- <Image
92
- src={imageUrl}
93
- alt={imageDoc.alt ?? member.name ?? ''}
94
- fill
95
- sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
96
- priority={Boolean(member.priority)}
97
- className="team-member object-cover object-top brightness-100 group-hover:brightness-50 group-hover:scale-110 transition-transform duration-500 ease-out"
98
- />
99
- </div>
100
- )}
101
- <div className="pattern-overlay absolute inset-0 opacity-[0.03] pointer-events-none" style={{ backgroundImage: 'repeating-linear-gradient(90deg, #000 0px, #000 1px, transparent 1px, transparent 20px)' }} />
102
- <div className="gradient-overlay absolute inset-0 bg-gradient-to-t from-black/80 via-black/80 to-transparent group-hover:from-black/90 transition-all duration-300 group-hover:backdrop-brightness-50" />
103
- <div className="content-card absolute inset-0 flex flex-col justify-end p-3 text-white">
104
- <div className="z-10 text-center mb-3">
105
- {member.name && <h3 className="text-xl md:text-2xl font-bold mb-1 text-white drop-shadow-lg" style={{ fontFamily: 'Outfit, sans-serif', fontWeight: 700 }}>{member.name}</h3>}
106
- {member.role && <p className="text-xs md:text-sm font-medium uppercase tracking-wider text-stone-200 dark:text-stone-200 drop-shadow-md" style={{ fontFamily: "'Inter', sans-serif", fontWeight: 500 }}>{member.role}</p>}
107
- </div>
108
- <div className="hover-panel transform translate-y-full group-hover:translate-y-0 transition-transform duration-300 ease-out p-4" style={{ background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(8px)' }}>
109
- {member.bio && <p className="text-[13px] text-white/70 leading-relaxed mb-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 delay-100">{member.bio}</p>}
110
- {socials.length > 0 && (
111
- <div className="socials-map flex gap-3 justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 delay-200">
112
- {socials.map(({ Icon, url, label }) => (
113
- <Link key={label} href={url!} target="_blank" rel="noopener noreferrer" aria-label={`${member.name} on ${label}`} 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">
114
- <Icon size={14} />
115
- </Link>
116
- ))}
117
- </div>
118
- )}
119
- </div>
120
- </div>
121
- </div>
122
- )
123
- })}
124
- </div>
125
- )}
126
- </div>
127
- </div>
128
- )
129
- }
130
-
131
- export default DDHGTeamCards
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