blocks-dusted 0.1.12 → 0.1.13

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 (148) hide show
  1. package/package.json +1 -1
  2. package/src/installers/patchRenderBlocks.js +1 -1
  3. package/src/registry/validateTemplateManifest.js +2 -1
  4. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  5. package/templates/blocks/BannerDD790432/config.ts +54 -0
  6. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  7. package/templates/blocks/Button47/Component.tsx +124 -0
  8. package/templates/blocks/Button47/config.ts +75 -0
  9. package/templates/blocks/Button47/link.ts +22 -0
  10. package/templates/blocks/Button47/manifest.json +105 -0
  11. package/templates/blocks/Button47/styles.module.scss +75 -0
  12. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  13. package/templates/blocks/DD-Accordion/config.ts +71 -59
  14. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  15. package/templates/blocks/DD-AnimText/config.ts +296 -295
  16. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  17. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  18. package/templates/blocks/DD-BookCall/Component.tsx +535 -535
  19. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  20. package/templates/blocks/DD-Card/Component.tsx +4 -4
  21. package/templates/blocks/DD-CardTemplate01/Component.tsx +139 -140
  22. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  23. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  24. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  25. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  28. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  29. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  30. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  31. package/templates/blocks/DD-Contact/Component.tsx +433 -435
  32. package/templates/blocks/DD-Counter/Component.tsx +11 -6
  33. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  34. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  35. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  36. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  37. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  38. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  39. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  40. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  41. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  42. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  43. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  44. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  45. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  46. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  47. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  48. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  49. package/templates/blocks/DD-Hero/Component.tsx +296 -297
  50. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  51. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  52. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  53. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  54. package/templates/blocks/DD-HorizontalScroll/Component.tsx +937 -936
  55. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  56. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  57. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  58. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  59. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  60. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  61. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -545
  62. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  63. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  64. package/templates/blocks/DD-Masonry/config.ts +475 -475
  65. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  66. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  67. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  68. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  69. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  70. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  71. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  72. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  73. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  74. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  75. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  76. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  77. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  78. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  79. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  80. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  81. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  82. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  83. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  84. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  85. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  86. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  87. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  88. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  89. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  90. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  91. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  92. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  95. package/templates/blocks/DD-Pricing/Component.tsx +2 -3
  96. package/templates/blocks/DD-Pricing/config.ts +215 -215
  97. package/templates/blocks/DD-Process/Component.tsx +2 -2
  98. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  99. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  100. package/templates/blocks/DD-Section/Component.tsx +265 -266
  101. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  102. package/templates/blocks/DD-Services/Component.tsx +176 -176
  103. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  104. package/templates/blocks/DD-Slider-A/Component.tsx +2 -2
  105. package/templates/blocks/DD-StackCards/Component.tsx +172 -171
  106. package/templates/blocks/DD-Team/Component.tsx +247 -247
  107. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  108. package/templates/blocks/DD-TechStack/Component.tsx +173 -174
  109. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  110. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  111. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  112. package/templates/blocks/DD-Testimonials/Component.tsx +139 -139
  113. package/templates/blocks/DD-Thanks/Component.tsx +1 -1
  114. package/templates/blocks/DD-Work/Component.tsx +318 -318
  115. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  116. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  117. package/templates/blocks/DDHG-HighlightArc/Component.tsx +6 -3
  118. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +45 -8
  119. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +1 -3
  120. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +9 -22
  121. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  122. package/templates/blocks/DDHG-TeamCards/Component.tsx +6 -35
  123. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  124. package/templates/blocks/Form/Component.tsx +163 -0
  125. package/templates/blocks/Form/Country/index.tsx +65 -0
  126. package/templates/blocks/Form/Country/options.ts +982 -0
  127. package/templates/blocks/Form/Email/index.tsx +38 -0
  128. package/templates/blocks/Form/Error/index.tsx +15 -0
  129. package/templates/blocks/Form/Message/index.tsx +13 -0
  130. package/templates/blocks/Form/Number/index.tsx +36 -0
  131. package/templates/blocks/Form/Select/index.tsx +63 -0
  132. package/templates/blocks/Form/State/index.tsx +64 -0
  133. package/templates/blocks/Form/State/options.ts +52 -0
  134. package/templates/blocks/Form/Text/index.tsx +32 -0
  135. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  136. package/templates/blocks/Form/Width/index.tsx +13 -0
  137. package/templates/blocks/Form/config.ts +51 -0
  138. package/templates/blocks/Form/fields.tsx +21 -0
  139. package/templates/blocks/Form/manifest.json +168 -0
  140. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  141. package/templates/blocks/MediaBlock/config.ts +14 -0
  142. package/templates/blocks/MediaBlock/manifest.json +92 -0
  143. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  144. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  145. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  146. package/templates/blocks/rawHtml/Component.tsx +25 -0
  147. package/templates/blocks/rawHtml/config.ts +56 -0
  148. package/templates/blocks/rawHtml/manifest.json +80 -0
@@ -0,0 +1,93 @@
1
+ {
2
+ "type": "block",
3
+ "name": "DD-FeatProd",
4
+ "label": "Featured Products",
5
+ "sourceName": "DD-FeatProd",
6
+ "portableName": "DD-FeatProd",
7
+ "slug": "DD-FeatProd",
8
+ "version": "0.0.1",
9
+ "source": {
10
+ "repo": "Hadizainal/Dusted-Payload-Blocks",
11
+ "originalPath": "CLI-TestGround/dusted-blocks-demo/src/blocks/DD-FeatProd",
12
+ "originalSlug": "DD-FeatProd",
13
+ "originalConfigExport": "DDFeatProd",
14
+ "originalComponentExport": "DDFeatProd",
15
+ "originalInterfaceName": "DDFeatProd"
16
+ },
17
+ "files": [
18
+ {
19
+ "from": "CircularGallery.tsx",
20
+ "to": "src/blocks/DD-FeatProd/CircularGallery.tsx"
21
+ },
22
+ {
23
+ "from": "Component.tsx",
24
+ "to": "src/blocks/DD-FeatProd/Component.tsx"
25
+ },
26
+ {
27
+ "from": "config.ts",
28
+ "to": "src/blocks/DD-FeatProd/config.ts"
29
+ }
30
+ ],
31
+ "dependencies": [
32
+ "embla-carousel-react",
33
+ "framer-motion",
34
+ "lucide-react",
35
+ "ogl"
36
+ ],
37
+ "sharedFiles": [],
38
+ "requiredProjectFiles": [
39
+ {
40
+ "path": "src/fields/link.ts",
41
+ "reason": "Payload config uses the shared link field."
42
+ },
43
+ {
44
+ "path": "src/utilities/ui.ts",
45
+ "reason": "Component imports cn from @/utilities/ui."
46
+ }
47
+ ],
48
+ "optionalProjectFiles": [],
49
+ "collectionRegistration": {
50
+ "enabled": true,
51
+ "collections": [
52
+ "Pages",
53
+ "Posts"
54
+ ]
55
+ },
56
+ "renderBlocksRegistration": {
57
+ "enabled": true
58
+ },
59
+ "renderBlocksBehavior": {
60
+ "wrapperless": false
61
+ },
62
+ "pageRouteIntegration": {
63
+ "required": false,
64
+ "manualSteps": []
65
+ },
66
+ "manual": [
67
+ "Register DDFeatProd in Pages/Posts block arrays if automatic registration was skipped.",
68
+ "Register DD-FeatProd in RenderBlocks if automatic registration was skipped.",
69
+ "Run payload generate:types after schema changes.",
70
+ "Run payload generate:importmap if admin/component imports changed."
71
+ ],
72
+ "manualSteps": [
73
+ "Register DDFeatProd in Pages/Posts block arrays if automatic registration was skipped.",
74
+ "Register DD-FeatProd in RenderBlocks if automatic registration was skipped.",
75
+ "Run payload generate:types after schema changes.",
76
+ "Run payload generate:importmap if admin/component imports changed."
77
+ ],
78
+ "patchTargets": {
79
+ "blockConfigRegistration": {
80
+ "description": "Installer can register DDFeatProd from @/blocks/DD-FeatProd/config."
81
+ },
82
+ "renderBlocksRegistration": {
83
+ "description": "Installer can register DD-FeatProd with component export DDFeatProd."
84
+ }
85
+ },
86
+ "notes": [
87
+ "Promoted from the verified Dusted Blocks demo implementation.",
88
+ "Installer preserves existing destination files and reports project requirements before writing."
89
+ ],
90
+ "exportName": "DDFeatProd",
91
+ "componentExportName": "DDFeatProd",
92
+ "interfaceName": "DDFeatProd"
93
+ }
@@ -0,0 +1,607 @@
1
+ 'use client'
2
+
3
+ import React, { useCallback, useEffect, useState } from 'react'
4
+ import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
5
+ import { motion } from 'framer-motion'
6
+ import useEmblaCarousel from 'embla-carousel-react'
7
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
8
+ import type { LucideIcon } from 'lucide-react'
9
+ import * as LucideIcons from 'lucide-react'
10
+ import Image from 'next/image'
11
+ import Link from 'next/link'
12
+ import { useRouter } from 'next/navigation'
13
+
14
+ import RichText from '@/components/RichText'
15
+ import { cn } from '@/utilities/ui'
16
+
17
+ /* ── Inlined CSS (converted from the original SCSS) ── */
18
+
19
+ const featuredServicesBlockStyles = `
20
+ .serviceHref-card.group {
21
+ box-shadow: var(--shadow-md);
22
+ border-radius: 1.5rem;
23
+ border: 1px solid var(--border);
24
+ corner-shape: ;
25
+ transition: all 0.3s;
26
+ }
27
+ .serviceHref-card.group .imageSrc-wrapper {
28
+ background: var(--bg-steel);
29
+ border-radius: 1.5rem 1.5rem;
30
+ background-repeat: repeat;
31
+ transition: all 0.3s;
32
+ corner-shape: ;
33
+ }
34
+ .serviceHref-card.group .imageSrc-wrapper img {
35
+ border-radius: 1rem 1rem 0 0 !important;
36
+ user-select: none;
37
+ corner-shape: ;
38
+ }
39
+ .serviceHref-card.group .card-content.serv-card {
40
+ backdrop-filter: blur(4px);
41
+ margin: -1rem 1rem 0;
42
+ z-index: 2;
43
+ border-radius: 1rem;
44
+ corner-shape: ;
45
+ transition: all 0.3s;
46
+ }
47
+
48
+ .featured-services-block .serviceHref-card.group .serviceContent {
49
+ overflow: visible;
50
+ }
51
+ .featured-services-block .serviceHref-card.group .serviceContent p {
52
+ display: -webkit-box;
53
+ max-height: calc(1.625em * 2);
54
+ overflow: hidden;
55
+ -webkit-box-orient: vertical;
56
+ -webkit-line-clamp: 2;
57
+ transition: max-height 0.3s ease;
58
+ }
59
+ .featured-services-block .serviceHref-card.group:hover .serviceContent p,
60
+ .featured-services-block .serviceHref-card.group:focus-within .serviceContent p {
61
+ display: block;
62
+ max-height: 20rem;
63
+ -webkit-line-clamp: unset;
64
+ }
65
+ .featured-services-block .serviceHref-card.group .serviceContent .button-custom-contained,
66
+ .featured-services-block .serviceHref-card.group .serviceContent .button-custom-outlined {
67
+ width: 100% !important;
68
+ max-width: none;
69
+ }
70
+ .featured-services-block .serviceHref-card.group .serviceContent .button-custom-contained-label {
71
+ flex: 1;
72
+ text-align: center;
73
+ }
74
+ .featured-services-block .serviceHref-card.group .serviceContent .relative.z-20,
75
+ .featured-services-block .serviceHref-card.group .serviceContent .relative.z-20 > div,
76
+ .featured-services-block .serviceHref-card.group .serviceContent .relative.z-20 > div > div {
77
+ width: 100%;
78
+ transform-origin: center;
79
+ }
80
+
81
+ .featured-services-block .serviceHref-card.group .serviceContent h4 {
82
+ margin-top:1rem;
83
+ }
84
+
85
+ .featured-services-block .serviceHref-card.group .serviceContent .accordion-wrapper {
86
+ box-shadow: var(--shadow-3xl);
87
+ margin-left: -1rem;
88
+ margin-right: -1rem;
89
+ padding: 0 1rem;
90
+ }
91
+
92
+
93
+
94
+ .serviceHref-card.group:hover {
95
+ box-shadow: none !important;
96
+ border: 0;
97
+ transform: translateY(-0.5rem);
98
+ margin-bottom: -1.5rem;
99
+ transition: all 0.3s;
100
+ background: transparent !important;
101
+ }
102
+ .serviceHref-card.group:hover .imageSrc-wrapper {
103
+ box-shadow: var(--shadow-2xl);
104
+ transition: all 0.3s;
105
+ }
106
+ .serviceHref-card.group:hover .card-content.serv-card {
107
+ transform: translateY(-1rem);
108
+ box-shadow: var(--shadow-2xl), inset 0px 0px 60px 11px #eee;
109
+ transition: all 0.6s;
110
+ }
111
+ .serviceHref-card.group:hover .cardButton {
112
+ scale: 1.05;
113
+ filter: brightness(1.1);
114
+ transition: all 0.4s;
115
+ }
116
+ .serviceHref-card.group ul.list-bullet {
117
+ margin: 1rem 0rem !important;
118
+ }
119
+ .serviceHref-card.group li {
120
+ position: relative;
121
+ list-style: none !important;
122
+ margin-bottom: 0.775rem !important;
123
+ padding-left: 1rem;
124
+ letter-spacing: 0.5px;
125
+ font-size: 0.775rem !important;
126
+ line-height: normal !important;
127
+ }
128
+ .serviceHref-card.group li::before {
129
+ content: "";
130
+ position: absolute;
131
+ left: 0;
132
+ top: 0.15em;
133
+ width: 1rem;
134
+ height: 1rem;
135
+ background: url("/assets/svg/icon-bfschevron-24.svg") center/contain no-repeat;
136
+ }
137
+
138
+ .serviceLink {
139
+ display: none;
140
+ }
141
+
142
+ .card-content.serv-card h4 span {
143
+ font-size: 2rem !important;
144
+ }
145
+
146
+ .viewAllHref-link:hover {
147
+ gap: 0.75rem;
148
+ transition: all 0.3s;
149
+ }
150
+ .viewAllHref-link:hover svg {
151
+ scale: 1.5;
152
+ transform-origin: right;
153
+ transition: all 0.2s;
154
+ }
155
+ `
156
+ const fadeUp = {
157
+ hidden: { opacity: 0, y: 40 },
158
+ visible: (i: number) => ({
159
+ opacity: 1,
160
+ y: 0,
161
+ transition: { duration: 0.5, ease: 'easeOut', delay: i * 0.1 },
162
+ }),
163
+ }
164
+
165
+ type MediaObj = {
166
+ url?: string | null
167
+ alt?: string | null
168
+ }
169
+
170
+ type CategoryValue = {
171
+ id?: string | number | null
172
+ title?: string | null
173
+ }
174
+
175
+ type LinkGroup = {
176
+ type?: 'custom' | 'reference' | null
177
+ newTab?: boolean | null
178
+ url?: string | null
179
+ label?: string | null
180
+ icon?: string | null
181
+ iconPosition?: 'before' | 'after' | 'only' | null
182
+ reference?: {
183
+ relationTo?: 'pages' | 'posts'
184
+ value?: { slug?: string | null } | string | number | null
185
+ } | null
186
+ }
187
+
188
+ type ServiceItem = {
189
+ id?: string | null
190
+ isClickableCard?: boolean | null
191
+ serviceLink: LinkGroup
192
+ serviceCategory?: CategoryValue | string | number | null
193
+ serviceContent: DefaultTypedEditorState
194
+ serviceBadge?: string | null
195
+ serviceImage?: MediaObj | string | null
196
+ serviceFeatures?:
197
+ | {
198
+ id?: string | null
199
+ serviceFeature: string
200
+ }[]
201
+ | null
202
+ }
203
+
204
+ export type DDFeatServProps = {
205
+ blockType?: 'DD-FeatServ'
206
+ blockName?: string | null
207
+ servicesEyebrow?: string | null
208
+ servicesHeading?: string | null
209
+ servicesDescription?: string | null
210
+ servicesViewAllLink?: LinkGroup | null
211
+ featuredServices?: ServiceItem[] | null
212
+ sectionId?: string | null
213
+ customCSS?: string | null
214
+ }
215
+
216
+ function getLinkHref(linkGroup?: LinkGroup | null): string | null {
217
+ if (
218
+ linkGroup?.type === 'reference' &&
219
+ linkGroup.reference?.value &&
220
+ typeof linkGroup.reference.value === 'object' &&
221
+ linkGroup.reference.value.slug
222
+ ) {
223
+ const collectionPrefix = linkGroup.reference.relationTo === 'posts' ? '/posts' : ''
224
+ return `${collectionPrefix}/${linkGroup.reference.value.slug}`
225
+ }
226
+
227
+ return linkGroup?.url || null
228
+ }
229
+
230
+ function getServiceLinkLabel(service: ServiceItem, index: number): string {
231
+ return (
232
+ service.serviceLink?.label ||
233
+ getServiceCategoryLabel(service) ||
234
+ service.serviceBadge ||
235
+ `Service ${index + 1}`
236
+ )
237
+ }
238
+
239
+ function getServiceCategoryLabel(service: ServiceItem): string | null {
240
+ if (service.serviceCategory && typeof service.serviceCategory === 'object') {
241
+ return service.serviceCategory.title || null
242
+ }
243
+
244
+ return typeof service.serviceCategory === 'string' ? service.serviceCategory : null
245
+ }
246
+
247
+ function createServicePlaceholder(_label?: string): string {
248
+ return `data:image/svg+xml,${encodeURIComponent(`
249
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="700" viewBox="0 0 1200 700">
250
+ <defs>
251
+ <linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
252
+ <stop offset="0" stop-color="#292524" />
253
+ <stop offset="1" stop-color="#4f46e5" />
254
+ </linearGradient>
255
+ </defs>
256
+ <rect width="1200" height="700" fill="url(#background)" />
257
+ <circle cx="1030" cy="120" r="250" fill="#ffffff" opacity="0.08" />
258
+ <circle cx="150" cy="650" r="300" fill="#ffffff" opacity="0.05" />
259
+ </svg>
260
+ `)}`
261
+ }
262
+
263
+ function getImageSrc(service: ServiceItem, index: number): string {
264
+ if (
265
+ service.serviceImage &&
266
+ typeof service.serviceImage === 'object' &&
267
+ service.serviceImage.url
268
+ ) {
269
+ return service.serviceImage.url
270
+ }
271
+
272
+ if (typeof service.serviceImage === 'string') {
273
+ return service.serviceImage
274
+ }
275
+
276
+ return createServicePlaceholder(getServiceLinkLabel(service, index))
277
+ }
278
+
279
+ function useSlidesPerView() {
280
+ const [slidesPerView, setSlidesPerView] = useState(3)
281
+
282
+ useEffect(() => {
283
+ const update = () => {
284
+ const width = window.innerWidth
285
+
286
+ if (width < 640) setSlidesPerView(1)
287
+ else if (width < 1024) setSlidesPerView(2)
288
+ else setSlidesPerView(3)
289
+ }
290
+
291
+ update()
292
+ window.addEventListener('resize', update)
293
+
294
+ return () => window.removeEventListener('resize', update)
295
+ }, [])
296
+
297
+ return slidesPerView
298
+ }
299
+
300
+ export const DDFeatServ: React.FC<DDFeatServProps> = ({
301
+ servicesEyebrow = 'What We Do',
302
+ servicesHeading = 'Our Services',
303
+ servicesDescription,
304
+ servicesViewAllLink,
305
+ featuredServices: servicesProp,
306
+ sectionId = 'featured-services',
307
+ customCSS,
308
+ }) => {
309
+ const router = useRouter()
310
+
311
+ useEffect(() => {
312
+ if (document.getElementById('featured-services-block-inline-styles')) return
313
+
314
+ const style = document.createElement('style')
315
+ style.id = 'featured-services-block-inline-styles'
316
+ style.textContent = featuredServicesBlockStyles
317
+ document.head.appendChild(style)
318
+
319
+ return () => style.remove()
320
+ }, [])
321
+
322
+ const services = servicesProp ?? []
323
+ const viewAllHref = getLinkHref(servicesViewAllLink)
324
+ const ViewAllIcon = servicesViewAllLink?.icon
325
+ ? (LucideIcons[servicesViewAllLink.icon as keyof typeof LucideIcons] as LucideIcon | undefined)
326
+ : undefined
327
+ const viewAllIconPosition = servicesViewAllLink?.iconPosition ?? 'after'
328
+ const slidesPerView = useSlidesPerView()
329
+
330
+ const [emblaRef, emblaApi] = useEmblaCarousel({
331
+ align: 'start',
332
+ slidesToScroll: 1,
333
+ containScroll: 'trimSnaps',
334
+ })
335
+
336
+ const [loadedImages, setLoadedImages] = useState<Record<number, boolean>>({})
337
+
338
+ const interactiveSelector =
339
+ 'a,button,input,select,textarea,[role="button"],[role="link"],[data-card-interactive]'
340
+
341
+ const handleCardNavigate = useCallback(
342
+ (
343
+ event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
344
+ service: ServiceItem,
345
+ ) => {
346
+ const target = event.target as HTMLElement
347
+
348
+ if (target.closest(interactiveSelector)) return
349
+
350
+ const serviceHref = getLinkHref(service.serviceLink)
351
+ const isClickableCard = service.isClickableCard !== false
352
+
353
+ if (!isClickableCard || !serviceHref) return
354
+
355
+ if (service.serviceLink.newTab) {
356
+ window.open(serviceHref, '_blank', 'noopener,noreferrer')
357
+ return
358
+ }
359
+
360
+ router.push(serviceHref)
361
+ },
362
+ [router],
363
+ )
364
+
365
+ const scrollPrev = useCallback(() => emblaApi?.scrollPrev(), [emblaApi])
366
+ const scrollNext = useCallback(() => emblaApi?.scrollNext(), [emblaApi])
367
+
368
+ if (!services.length) return null
369
+
370
+ return (
371
+ <div
372
+ id={sectionId ?? undefined}
373
+ className="featured-services-block relative left-0 mx-auto w-screen max-w-none overflow-hidden py-20 text-stone-900 dark:text-stone-100"
374
+ >
375
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
376
+
377
+ <div className="container-fluid">
378
+ <div className="mx-auto max-w-7xl px-4 md:px-6">
379
+ {(servicesEyebrow || servicesHeading || servicesDescription || viewAllHref) && (
380
+ <motion.div
381
+ className="mb-12 flex flex-col justify-between gap-4 md:flex-row md:items-end"
382
+ initial="hidden"
383
+ whileInView="visible"
384
+ viewport={{ once: true, amount: 0.3 }}
385
+ custom={0}
386
+ variants={fadeUp}
387
+ >
388
+ <div>
389
+ {servicesEyebrow && (
390
+ <div
391
+ data-type={`eyebrow-${servicesEyebrow}`}
392
+ className="text-xs tracking-widest text-indigo-700 dark:text-indigo-300"
393
+ >
394
+ {servicesEyebrow}
395
+ </div>
396
+ )}
397
+
398
+ {servicesHeading && (
399
+ <h2 className="text-3xl font-black leading-tight text-stone-900 dark:text-stone-100 md:text-4xl lg:text-5xl">
400
+ {servicesHeading}
401
+ </h2>
402
+ )}
403
+
404
+ {servicesDescription && (
405
+ <p className="mt-3 max-w-xl text-sm text-stone-600 dark:text-stone-300 md:text-base">
406
+ {servicesDescription}
407
+ </p>
408
+ )}
409
+ </div>
410
+
411
+ <div className="flex items-center gap-3">
412
+ <button
413
+ type="button"
414
+ onClick={scrollPrev}
415
+ aria-label="Previous service"
416
+ className="flex h-11 w-11 cursor-pointer items-center justify-center rounded-full border-2 border-stone-800 text-stone-900 transition-all hover:bg-stone-900 hover:text-stone-50 dark:border-stone-200 dark:text-stone-100 dark:hover:bg-stone-100 dark:hover:text-stone-950 disabled:cursor-not-allowed disabled:opacity-30"
417
+ >
418
+ <ChevronLeft className="h-5 w-5" />
419
+ </button>
420
+
421
+ <button
422
+ type="button"
423
+ onClick={scrollNext}
424
+ aria-label="Next service"
425
+ className="flex h-11 w-11 cursor-pointer items-center justify-center rounded-full bg-indigo-600 text-white transition-all hover:bg-indigo-700 dark:bg-indigo-500 dark:text-stone-950 dark:hover:bg-indigo-300 disabled:cursor-not-allowed disabled:opacity-30"
426
+ >
427
+ <ChevronRight className="h-5 w-5" />
428
+ </button>
429
+
430
+ {viewAllHref && servicesViewAllLink?.label && (
431
+ <Link
432
+ href={viewAllHref}
433
+ aria-label={
434
+ viewAllIconPosition === 'only' ? servicesViewAllLink.label : undefined
435
+ }
436
+ className="viewAllHref-link ml-2 inline-flex items-center gap-1.5 whitespace-nowrap text-sm font-semibold text-stone-900 dark:text-stone-100 transition-colors hover:text-indigo-700 dark:text-stone-100 dark:hover:text-indigo-300"
437
+ {...(servicesViewAllLink.newTab
438
+ ? { rel: 'noopener noreferrer', target: '_blank' }
439
+ : {})}
440
+ >
441
+ {ViewAllIcon && viewAllIconPosition === 'before' && (
442
+ <ViewAllIcon aria-hidden="true" className="h-4 w-4 shrink-0" />
443
+ )}
444
+ {viewAllIconPosition !== 'only' && servicesViewAllLink.label}
445
+ {ViewAllIcon && ['after', 'only'].includes(viewAllIconPosition) && (
446
+ <ViewAllIcon aria-hidden="true" className="h-4 w-4 shrink-0" />
447
+ )}
448
+ {!ViewAllIcon && viewAllIconPosition !== 'only' && (
449
+ <span aria-hidden="true">
450
+ <svg
451
+ width="9"
452
+ height="18"
453
+ viewBox="0 0 9 18"
454
+ fill="none"
455
+ xmlns="http://www.w3.org/2000/svg"
456
+ >
457
+ <path
458
+ d="M5.53846 0H0L3.46154 8.86957L0 18H5.53846L9 8.86957L5.53846 0Z"
459
+ fill="currentColor"
460
+ />
461
+ </svg>
462
+ </span>
463
+ )}
464
+ </Link>
465
+ )}
466
+ </div>
467
+ </motion.div>
468
+ )}
469
+
470
+ <div
471
+ ref={emblaRef}
472
+ data-container-type="serviceCarouselContainer"
473
+ className="overflow-x-visible overflow-y-visible pb-8"
474
+ >
475
+ <div className="flex py-4">
476
+ {services.map((service, index) => {
477
+ const imageSrc = getImageSrc(service, index)
478
+ const serviceHref = getLinkHref(service.serviceLink)
479
+ const serviceCategoryLabel = getServiceCategoryLabel(service)
480
+ const serviceLinkLabel = getServiceLinkLabel(service, index)
481
+ const isClickableCard = service.isClickableCard !== false && !!serviceHref
482
+
483
+ return (
484
+ <motion.div
485
+ key={service.id ?? service.serviceLink?.label ?? serviceCategoryLabel ?? index}
486
+ className={cn(
487
+ 'slidesWidth min-w-0 flex-none px-2',
488
+ slidesPerView === 1 && 'w-full',
489
+ slidesPerView === 2 && 'w-1/2',
490
+ slidesPerView === 3 && 'w-1/3',
491
+ )}
492
+ initial="hidden"
493
+ whileInView="visible"
494
+ viewport={{ once: true, amount: 0.2 }}
495
+ custom={index}
496
+ variants={fadeUp}
497
+ >
498
+ <div
499
+ className={cn(
500
+ 'serviceHref-card group relative block h-fit my-4',
501
+ isClickableCard &&
502
+ 'cursor-pointer focus-visible:outline focus-visible:outline-2 outline-4 outline-solid focus-visible:outline-offset-2 focus-visible:outline-[#4f46e5]',
503
+ )}
504
+ role={isClickableCard ? 'link' : undefined}
505
+ tabIndex={isClickableCard ? 0 : undefined}
506
+ aria-label={isClickableCard ? serviceLinkLabel : undefined}
507
+ onClick={
508
+ isClickableCard ? (event) => handleCardNavigate(event, service) : undefined
509
+ }
510
+ onKeyDown={
511
+ isClickableCard
512
+ ? (event) => {
513
+ if (event.key !== 'Enter' && event.key !== ' ') return
514
+
515
+ event.preventDefault()
516
+ handleCardNavigate(event, service)
517
+ }
518
+ : undefined
519
+ }
520
+ >
521
+ <div className="imageSrc-wrapper flex h-full flex-col overflow-hidden border border-stone-200 dark:border-stone-700 bg-stone-50 text-stone-900 transition-all duration-300 dark:bg-stone-900 dark:text-stone-100 hover:-translate-y-2 hover:border-indigo-500/40 hover:shadow-xl">
522
+ <div className="imageSrc rounded-t-2xl relative h-52 flex-shrink-0 overflow-hidden bg-stone-100 dark:bg-stone-800">
523
+ {imageSrc ? (
524
+ <>
525
+ {!loadedImages[index] && (
526
+ <div className="absolute inset-0 animate-pulse bg-stone-200 dark:bg-stone-800 overflow-hidden" />
527
+ )}
528
+
529
+ <Image
530
+ src={imageSrc}
531
+ alt={
532
+ service.serviceImage && typeof service.serviceImage === 'object'
533
+ ? service.serviceImage.alt || serviceLinkLabel
534
+ : serviceLinkLabel
535
+ }
536
+ fill
537
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
538
+ priority={index === 0}
539
+ unoptimized={imageSrc.startsWith('data:')}
540
+ className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-105 rounded-t-2xl overflow-hidden"
541
+ draggable={false}
542
+ onLoad={() =>
543
+ setLoadedImages((previous) => ({
544
+ ...previous,
545
+ [index]: true,
546
+ }))
547
+ }
548
+ />
549
+ </>
550
+ ) : (
551
+ <div className="h-full w-full bg-stone-200 dark:bg-stone-800" />
552
+ )}
553
+
554
+ {service.serviceBadge && (
555
+ <div className="serviceBadge absolute left-3 top-0 rounded-b-md shadow-sm bg-indigo-600 uppercase tracking-wide px-2 pt-1 pb-0.5 text-xs font-semibold text-white">
556
+ {service.serviceBadge}
557
+ </div>
558
+ )}
559
+
560
+ <div className="absolute inset-0 bg-gradient-to-t from-[#292524]/60 via-transparent to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
561
+
562
+ <div className="serviceLink absolute bottom-3 left-3 right-3 opacity-0 transition-opacity duration-300 group-hover:opacity-100 hidden">
563
+ {/* <span className="text-sm font-semibold text-white">
564
+ {serviceLinkLabel}
565
+ </span> */}
566
+ </div>
567
+ </div>
568
+
569
+ <div className="card-content serv-card flex bg-stone-200/50 dark:bg-stone-900/50 dark:group-hover:bg-stone-100/80 group-hover:shadow-2xl flex-1 flex-col px-4 pb-5 pt-2">
570
+ {serviceCategoryLabel && (
571
+ <div className="mb-1 w-fit -mt-4 rounded-lg bg-stone-700 dark:bg-stone-300 px-2 py-[0.2rem] text-xs font-semibold uppercase tracking-widest text-indigo-700 dark:text-indigo-300 [box-shadow:var(--shadow-xl)] [text-shadow:0_1px_0_#8a7f7f] [corner-shape:] scale-90 origin-top-left">
572
+ {serviceCategoryLabel}
573
+ </div>
574
+ )}
575
+
576
+ <RichText
577
+ data={service.serviceContent}
578
+ enableGutter={false}
579
+ enableProse={false}
580
+ className="serviceContent mb-4 flex-1 [&_a]:relative [&_a]:z-20 [&_h4]:mb-2 [&_h4]:line-clamp-none [&_h4]:text-base [&_h4]:font-bold [&_h4]:leading-snug [&_h4]:text-stone-900 dark:[&_h4]:text-stone-100 dark:group-hover:[&_h4]:text-stone-900 dark:text-stone-500 [&_p]:line-clamp-none [&_p]:text-sm [&_p]:leading-relaxed [&_p]:text-stone-700 dark:[&_p]:text-stone-300 dark:group-hover:[&_p]:text-stone-700 dark:hover:text-stone-900"
581
+ />
582
+
583
+ {!!service.serviceFeatures?.length && (
584
+ <div className="flex flex-wrap gap-1.5">
585
+ {service.serviceFeatures.slice(0, 2).map((item) => (
586
+ <span
587
+ key={item.id ?? item.serviceFeature}
588
+ className="rounded-full bg-indigo-100 dark:bg-indigo-950/70 px-2.5 py-1 text-xs text-indigo-700 dark:text-indigo-300"
589
+ >
590
+ {item.serviceFeature}
591
+ </span>
592
+ ))}
593
+ </div>
594
+ )}
595
+ </div>
596
+ </div>
597
+ </div>
598
+ </motion.div>
599
+ )
600
+ })}
601
+ </div>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ </div>
606
+ )
607
+ }