blocks-dusted 0.1.11 → 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 (161) 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/src/installers/patchRenderBlocks.js +1 -1
  6. package/src/registry/validateTemplateManifest.js +2 -1
  7. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  8. package/templates/blocks/BannerDD790432/config.ts +54 -0
  9. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  10. package/templates/blocks/Button47/Component.tsx +124 -0
  11. package/templates/blocks/Button47/config.ts +75 -0
  12. package/templates/blocks/Button47/link.ts +22 -0
  13. package/templates/blocks/Button47/manifest.json +105 -0
  14. package/templates/blocks/Button47/styles.module.scss +75 -0
  15. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  16. package/templates/blocks/DD-Accordion/config.ts +71 -59
  17. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  18. package/templates/blocks/DD-AnimText/config.ts +296 -295
  19. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  20. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  21. package/templates/blocks/DD-BookCall/Component.tsx +10 -9
  22. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  23. package/templates/blocks/DD-Card/Component.tsx +4 -4
  24. package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -5
  25. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  28. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  29. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  30. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  31. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  32. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  33. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  34. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  35. package/templates/blocks/DD-Contact/Component.tsx +433 -434
  36. package/templates/blocks/DD-Counter/Component.tsx +18 -13
  37. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  38. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  39. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  40. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  41. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  42. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  43. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  44. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  45. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  46. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  47. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  48. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  49. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  50. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  51. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  52. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  53. package/templates/blocks/DD-Hero/Component.tsx +16 -40
  54. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  55. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  56. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  57. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  58. package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -5
  59. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  60. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  61. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  62. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  63. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  64. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  65. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  66. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -547
  67. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  68. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  69. package/templates/blocks/DD-Masonry/config.ts +475 -475
  70. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  71. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  72. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  73. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  74. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  75. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  76. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  77. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  78. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  79. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  80. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  81. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  82. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  83. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  84. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  85. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  86. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  87. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  88. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  89. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  90. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  91. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  92. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  95. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  96. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  97. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  98. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  99. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  100. package/templates/blocks/DD-Pricing/Component.tsx +368 -372
  101. package/templates/blocks/DD-Pricing/config.ts +215 -215
  102. package/templates/blocks/DD-Process/Component.tsx +147 -149
  103. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  104. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  105. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  106. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  107. package/templates/blocks/DD-Section/Component.tsx +265 -266
  108. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  109. package/templates/blocks/DD-Services/Component.tsx +176 -176
  110. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  111. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  112. package/templates/blocks/DD-StackCards/Component.tsx +474 -473
  113. package/templates/blocks/DD-Team/Component.tsx +247 -247
  114. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  115. package/templates/blocks/DD-TechStack/Component.tsx +161 -168
  116. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  117. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  118. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  119. package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
  120. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  121. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  122. package/templates/blocks/DD-Work/Component.tsx +315 -315
  123. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  124. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  125. package/templates/blocks/DDHG-HighlightArc/Component.tsx +246 -243
  126. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +180 -93
  127. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  128. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +225 -213
  129. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  130. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  131. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +30 -47
  132. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  133. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  134. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  135. package/templates/blocks/DDHG-TeamCards/Component.tsx +140 -131
  136. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  137. package/templates/blocks/Form/Component.tsx +163 -0
  138. package/templates/blocks/Form/Country/index.tsx +65 -0
  139. package/templates/blocks/Form/Country/options.ts +982 -0
  140. package/templates/blocks/Form/Email/index.tsx +38 -0
  141. package/templates/blocks/Form/Error/index.tsx +15 -0
  142. package/templates/blocks/Form/Message/index.tsx +13 -0
  143. package/templates/blocks/Form/Number/index.tsx +36 -0
  144. package/templates/blocks/Form/Select/index.tsx +63 -0
  145. package/templates/blocks/Form/State/index.tsx +64 -0
  146. package/templates/blocks/Form/State/options.ts +52 -0
  147. package/templates/blocks/Form/Text/index.tsx +32 -0
  148. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  149. package/templates/blocks/Form/Width/index.tsx +13 -0
  150. package/templates/blocks/Form/config.ts +51 -0
  151. package/templates/blocks/Form/fields.tsx +21 -0
  152. package/templates/blocks/Form/manifest.json +168 -0
  153. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  154. package/templates/blocks/MediaBlock/config.ts +14 -0
  155. package/templates/blocks/MediaBlock/manifest.json +92 -0
  156. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  157. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  158. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  159. package/templates/blocks/rawHtml/Component.tsx +25 -0
  160. package/templates/blocks/rawHtml/config.ts +56 -0
  161. package/templates/blocks/rawHtml/manifest.json +80 -0
@@ -0,0 +1,484 @@
1
+ 'use client'
2
+
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react'
4
+ import Image from 'next/image'
5
+ import Link from 'next/link'
6
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
7
+ import type { LucideIcon } from 'lucide-react'
8
+ import * as LucideIcons from 'lucide-react'
9
+ import useEmblaCarousel from 'embla-carousel-react'
10
+ import { motion } from 'framer-motion'
11
+ import { cn } from '@/utilities/ui'
12
+ import dynamic from 'next/dynamic'
13
+
14
+ /* -- Inlined CSS (converted from the original SCSS) -- */
15
+
16
+ const featuredProductsBlockStyles = `
17
+ .viewAllHref-link {
18
+ margin-left: 4rem;
19
+ padding: 1rem;
20
+ border-radius: 1.5rem;
21
+ corner-shape: squircle;
22
+ transition: all 0.3s;
23
+
24
+ @media (max-width: 768px) {
25
+ right: 1rem;
26
+ position: absolute;
27
+ float: right;
28
+ background: transparent;
29
+ border: 1px solid transparent;
30
+ }
31
+ }
32
+ .viewAllHref-link:hover {
33
+ gap: 0.75rem;
34
+ transition: all 0.3s;
35
+ }
36
+ .viewAllHref-link:hover svg {
37
+ scale: 1.5;
38
+ transform-origin: right;
39
+ transition: all 0.2s;
40
+ }
41
+
42
+ [data-container-type="productCarouselContainer"] .productCardWrapper.isSelected {
43
+ [data-content-type="featuredProductCard"] {
44
+ outline: 3px solid indigo;
45
+ transition: all 0.3s;
46
+ }
47
+ }
48
+
49
+ `
50
+ const CircularGallery = dynamic(() => import('./CircularGallery'), { ssr: false })
51
+
52
+ const fadeUp = {
53
+ hidden: { opacity: 0, y: 40 },
54
+ visible: (i: number) => ({
55
+ opacity: 1,
56
+ y: 0,
57
+ transition: { duration: 0.5, ease: 'easeOut', delay: i * 0.1 },
58
+ }),
59
+ }
60
+
61
+ type MediaObj = {
62
+ url?: string | null
63
+ alt?: string | null
64
+ }
65
+
66
+ type LinkGroup = {
67
+ type?: 'custom' | 'reference' | null
68
+ newTab?: boolean | null
69
+ url?: string | null
70
+ label?: string | null
71
+ icon?: string | null
72
+ iconPosition?: 'before' | 'after' | 'only' | null
73
+ reference?: {
74
+ relationTo?: 'pages' | 'posts'
75
+ value?: { slug?: string | null } | string | number | null
76
+ } | null
77
+ }
78
+
79
+ type ProductItem = {
80
+ id?: string | null
81
+ name: string
82
+ slug: string
83
+ category?: string | null
84
+ shortDesc?: string | null
85
+ badge?: string | null
86
+ image?: MediaObj | string | null
87
+ imageFallbackUrl?: string | null
88
+ features?:
89
+ | {
90
+ id?: string | null
91
+ feature: string
92
+ }[]
93
+ | null
94
+ }
95
+
96
+ export type DDFeatProdProps = {
97
+ blockType?: 'DD-FeatProd'
98
+ blockName?: string | null
99
+ eyebrow?: string | null
100
+ heading?: string | null
101
+ description?: string | null
102
+ viewAllLabel?: string | null
103
+ viewAllUrl?: LinkGroup | string | null
104
+ products?: ProductItem[] | null
105
+ sectionId?: string | null
106
+ customCSS?: string | null
107
+ }
108
+
109
+ function getLinkHref(linkGroup?: LinkGroup | string | null): string | null {
110
+ if (typeof linkGroup === 'string') return linkGroup
111
+
112
+ if (
113
+ linkGroup?.type === 'reference' &&
114
+ linkGroup.reference?.value &&
115
+ typeof linkGroup.reference.value === 'object' &&
116
+ linkGroup.reference.value.slug
117
+ ) {
118
+ const collectionPrefix = linkGroup.reference.relationTo === 'posts' ? '/posts' : ''
119
+ return `${collectionPrefix}/${linkGroup.reference.value.slug}`
120
+ }
121
+
122
+ return linkGroup?.url || null
123
+ }
124
+
125
+ function getImageSrc(product: ProductItem): string {
126
+ if (product.image && typeof product.image === 'object' && (product.image as MediaObj).url) {
127
+ return (product.image as MediaObj).url!
128
+ }
129
+
130
+ if (typeof product.image === 'string') {
131
+ return product.image
132
+ }
133
+
134
+ return product.imageFallbackUrl ?? ''
135
+ }
136
+
137
+ function useSlidesPerView() {
138
+ const [spv, setSpv] = useState(3)
139
+
140
+ useEffect(() => {
141
+ const update = () => {
142
+ const width = window.innerWidth
143
+
144
+ if (width < 640) setSpv(1)
145
+ else if (width < 1024) setSpv(2)
146
+ else setSpv(3)
147
+ }
148
+
149
+ update()
150
+ window.addEventListener('resize', update)
151
+
152
+ return () => window.removeEventListener('resize', update)
153
+ }, [])
154
+
155
+ return spv
156
+ }
157
+
158
+ export const DDFeatProd: React.FC<DDFeatProdProps> = ({
159
+ eyebrow = 'What We Supply',
160
+ heading = 'Our Product Range',
161
+ description = 'Industry-leading plastic sheet materials for every application - from signage to industrial packaging.',
162
+ viewAllLabel = 'View All',
163
+ viewAllUrl,
164
+ products: productsProp,
165
+ sectionId,
166
+ customCSS,
167
+ }) => {
168
+ useEffect(() => {
169
+ if (document.getElementById('featured-products-block-inline-styles')) return
170
+
171
+ const style = document.createElement('style')
172
+ style.id = 'featured-products-block-inline-styles'
173
+ style.textContent = featuredProductsBlockStyles
174
+ document.head.appendChild(style)
175
+
176
+ return () => style.remove()
177
+ }, [])
178
+
179
+ const products = productsProp ?? []
180
+ const viewAllLink = typeof viewAllUrl === 'object' ? viewAllUrl : null
181
+ const viewAllHref = getLinkHref(viewAllUrl)
182
+ const resolvedViewAllLabel = viewAllLink?.label || viewAllLabel || 'View All'
183
+ const ViewAllIcon = viewAllLink?.icon
184
+ ? (LucideIcons[viewAllLink.icon as keyof typeof LucideIcons] as LucideIcon | undefined)
185
+ : undefined
186
+ const viewAllIconPosition = viewAllLink?.iconPosition ?? 'after'
187
+ const slidesPerView = useSlidesPerView()
188
+
189
+ const [mainEmblaRef, mainEmblaApi] = useEmblaCarousel({
190
+ align: 'start',
191
+ slidesToScroll: 1,
192
+ containScroll: 'trimSnaps',
193
+ })
194
+
195
+ const [selectedIndex, setSelectedIndex] = useState(0)
196
+ const [loadedImages, setLoadedImages] = useState<Record<number, boolean>>({})
197
+
198
+ const onSelect = useCallback(() => {
199
+ if (!mainEmblaApi) return
200
+ const index = mainEmblaApi.selectedScrollSnap()
201
+ setSelectedIndex(index)
202
+ }, [mainEmblaApi])
203
+
204
+ useEffect(() => {
205
+ if (!mainEmblaApi) return
206
+
207
+ onSelect()
208
+ mainEmblaApi.on('select', onSelect)
209
+ mainEmblaApi.on('reInit', onSelect)
210
+
211
+ return () => {
212
+ mainEmblaApi.off('select', onSelect)
213
+ mainEmblaApi.off('reInit', onSelect)
214
+ }
215
+ }, [mainEmblaApi, onSelect])
216
+
217
+ const scrollPrev = useCallback(() => mainEmblaApi?.scrollPrev(), [mainEmblaApi])
218
+ const scrollNext = useCallback(() => mainEmblaApi?.scrollNext(), [mainEmblaApi])
219
+ const scrollTo = useCallback(
220
+ (index: number) => {
221
+ setSelectedIndex(index)
222
+ mainEmblaApi?.scrollTo(index)
223
+ },
224
+ [mainEmblaApi],
225
+ )
226
+
227
+ const galleryItems = useMemo(
228
+ () =>
229
+ products.map((p) => ({
230
+ image: getImageSrc(p),
231
+ text: p.name.length > 24 ? p.name.slice(0, 24) + '\u2026' : p.name,
232
+ })),
233
+ // eslint-disable-next-line react-hooks/exhaustive-deps
234
+ [products.map((p) => p.slug).join(',')],
235
+ )
236
+
237
+ if (!products.length) return null
238
+
239
+ return (
240
+ <div
241
+ id={sectionId ?? undefined}
242
+ className="featured-products-block relative left-0 mx-auto w-screen max-w-none overflow-hidden bg-stone-50 py-20 text-stone-900 dark:bg-stone-950 dark:text-stone-100"
243
+ >
244
+ {customCSS && <style dangerouslySetInnerHTML={{ __html: customCSS }} />}
245
+
246
+ <div className="container-fluid">
247
+ <div className="mx-auto max-w-7xl px-4 md:px-6">
248
+ {(eyebrow || heading || description || viewAllHref) && (
249
+ <motion.div
250
+ className="mb-0 flex flex-col justify-between gap-4 md:flex-row md:items-end"
251
+ initial="hidden"
252
+ whileInView="visible"
253
+ viewport={{ once: true, amount: 0.3 }}
254
+ custom={0}
255
+ // variants={fadeUp}
256
+ >
257
+ <div>
258
+ {eyebrow && (
259
+ <div
260
+ data-type={`eyebrow-${eyebrow}`}
261
+ className="text-xs tracking-widest text-indigo-700 dark:text-indigo-300"
262
+ >
263
+ {eyebrow}
264
+ </div>
265
+ )}
266
+
267
+ {heading && (
268
+ <h2 className="text-3xl font-black leading-tight text-stone-900 dark:text-stone-100 md:text-4xl lg:text-5xl">
269
+ {heading}
270
+ </h2>
271
+ )}
272
+
273
+ {description && (
274
+ <p className="mt-3 max-w-xl text-sm text-stone-600 dark:text-stone-300 md:text-base">
275
+ {description}
276
+ </p>
277
+ )}
278
+ </div>
279
+
280
+ <div className="flex items-center gap-3">
281
+ <button
282
+ type="button"
283
+ onClick={scrollPrev}
284
+ aria-label="Previous product"
285
+ 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"
286
+ >
287
+ <ChevronLeft className="h-5 w-5" />
288
+ </button>
289
+
290
+ <button
291
+ type="button"
292
+ onClick={scrollNext}
293
+ aria-label="Next product"
294
+ 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"
295
+ >
296
+ <ChevronRight className="h-5 w-5" />
297
+ </button>
298
+
299
+ {viewAllHref && resolvedViewAllLabel && (
300
+ <Link
301
+ href={viewAllHref}
302
+ aria-label={viewAllIconPosition === 'only' ? resolvedViewAllLabel : undefined}
303
+ 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"
304
+ {...(viewAllLink?.newTab
305
+ ? { rel: 'noopener noreferrer', target: '_blank' }
306
+ : {})}
307
+ >
308
+ {ViewAllIcon && viewAllIconPosition === 'before' && (
309
+ <ViewAllIcon aria-hidden="true" className="h-4 w-4 shrink-0" />
310
+ )}
311
+ {viewAllIconPosition !== 'only' && resolvedViewAllLabel}
312
+ {ViewAllIcon && ['after', 'only'].includes(viewAllIconPosition) && (
313
+ <ViewAllIcon aria-hidden="true" className="h-4 w-4 shrink-0" />
314
+ )}
315
+ {!ViewAllIcon && viewAllIconPosition !== 'only' && (
316
+ <span aria-hidden="true">
317
+ <svg
318
+ width="9"
319
+ height="18"
320
+ viewBox="0 0 9 18"
321
+ fill="none"
322
+ xmlns="http://www.w3.org/2000/svg"
323
+ >
324
+ <path
325
+ d="M5.53846 0H0L3.46154 8.86957L0 18H5.53846L9 8.86957L5.53846 0Z"
326
+ fill="currentColor"
327
+ />
328
+ </svg>
329
+ </span>
330
+ )}
331
+ </Link>
332
+ )}
333
+ </div>
334
+ </motion.div>
335
+ )}
336
+
337
+ <div
338
+ ref={mainEmblaRef}
339
+ data-container-type="productCarouselContainer"
340
+ className="overflow-x-visible overflow-y-visible pb-8"
341
+ >
342
+ <div className="flex py-4">
343
+ {products.map((product, index) => {
344
+ const imgSrc = getImageSrc(product)
345
+
346
+ return (
347
+ <motion.div
348
+ key={product.id ?? product.slug ?? index}
349
+ data-content-type="productCardWrapper"
350
+ className={cn(
351
+ 'productCardWrapper',
352
+ 'min-w-0 flex-none px-2',
353
+ selectedIndex === index && 'isSelected',
354
+ slidesPerView === 1 && 'w-full',
355
+ slidesPerView === 2 && 'w-1/2',
356
+ slidesPerView === 3 && 'w-1/3',
357
+ )}
358
+ initial="hidden"
359
+ whileInView="visible"
360
+ viewport={{ once: true, amount: 0.2 }}
361
+ custom={index}
362
+ // variants={fadeUp}
363
+ >
364
+ <Link
365
+ href={`${product.slug}`}
366
+ // href={`/products/${product.slug}`}
367
+ className="group block h-full"
368
+ data-link-type="featuredProductCard"
369
+ >
370
+ <div
371
+ data-content-type="featuredProductCard"
372
+ className="flex h-full flex-col overflow-hidden rounded-2xl 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/50 hover:border-[2px] hover:shadow-xl"
373
+ >
374
+ <div className="relative h-52 flex-shrink-0 overflow-hidden bg-stone-100 dark:bg-stone-800">
375
+ {imgSrc ? (
376
+ <>
377
+ {!loadedImages[index] && (
378
+ <div className="absolute shimmer-effect inset-0 z-1 flex items-center justify-center bg-steel rounded-sm">
379
+ <div className="flex flex-col items-center gap-3">
380
+ <div className="text-stone-500 text-sm font-medium px-4 py-2 rounded-md">
381
+ Loading video...
382
+ </div>
383
+ </div>
384
+ </div>
385
+ // <div className="absolute inset-0 animate-pulse bg-stone-200 dark:bg-stone-800" />
386
+ )}
387
+
388
+ <Image
389
+ src={imgSrc}
390
+ alt={
391
+ product.image && typeof product.image === 'object'
392
+ ? product.image.alt || product.name
393
+ : product.name
394
+ }
395
+ fill
396
+ sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
397
+ priority={index === 0}
398
+ className="h-full w-full object-cover object-top transition-transform duration-500 group-hover:scale-105"
399
+ draggable={false}
400
+ onLoad={() =>
401
+ setLoadedImages((prev) => ({
402
+ ...prev,
403
+ [index]: true,
404
+ }))
405
+ }
406
+ />
407
+ </>
408
+ ) : (
409
+ <div className="h-full w-full bg-stone-200 dark:bg-stone-800" />
410
+ )}
411
+
412
+ {product.badge && (
413
+ <div className="absolute left-3 top-3 rounded-full bg-indigo-600 px-3 py-1 text-xs font-bold text-white">
414
+ {product.badge}
415
+ </div>
416
+ )}
417
+
418
+ <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" />
419
+
420
+ <div className="absolute bottom-3 left-3 right-3 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
421
+ <span className="text-sm font-semibold text-white">
422
+ View Details &gt;
423
+ </span>
424
+ </div>
425
+ </div>
426
+
427
+ <div className="card-content prod-card flex flex-1 flex-col px-4 pb-5 pt-2">
428
+ {product.category && (
429
+ <div className="mb-1 text-xs uppercase tracking-widest text-indigo-700 dark:text-indigo-300">
430
+ {product.category}
431
+ </div>
432
+ )}
433
+
434
+ <h3 className="mb-2 line-clamp-2 text-base font-bold leading-snug text-stone-900 dark:text-stone-100">
435
+ {product.name}
436
+ </h3>
437
+
438
+ {product.shortDesc && (
439
+ <p className="mb-4 line-clamp-2 flex-1 text-sm leading-relaxed text-stone-600 dark:text-stone-300">
440
+ {product.shortDesc}
441
+ </p>
442
+ )}
443
+
444
+ {!!product.features?.length && (
445
+ <div className="flex flex-wrap gap-1.5">
446
+ {product.features.slice(0, 2).map((item) => (
447
+ <span
448
+ key={item.id ?? item.feature}
449
+ 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"
450
+ >
451
+ {item.feature}
452
+ </span>
453
+ ))}
454
+ </div>
455
+ )}
456
+ </div>
457
+ </div>
458
+ </Link>
459
+ </motion.div>
460
+ )
461
+ })}
462
+ </div>
463
+ </div>
464
+ </div>
465
+
466
+ {products.length > 1 && (
467
+ <div className="mt-2 h-64 w-full overflow-visible relative top-[-4rem]">
468
+ <CircularGallery
469
+ items={galleryItems}
470
+ bend={2}
471
+ textColor="#432dd7"
472
+ borderRadius={0.05}
473
+ scrollSpeed={2}
474
+ scrollEase={0.05}
475
+ onItemClick={(idx) => scrollTo(idx)}
476
+ />
477
+ </div>
478
+ )}
479
+ </div>
480
+ </div>
481
+ )
482
+ }
483
+
484
+ export default DDFeatProd
@@ -0,0 +1,173 @@
1
+ import type { Block } from 'payload'
2
+ import { link } from '@/fields/link'
3
+
4
+ export const DDFeatProd: Block = {
5
+ slug: 'DD-FeatProd',
6
+ dbName: 'dd_feat_prod',
7
+ interfaceName: 'DDFeatProd',
8
+ labels: {
9
+ singular: 'DD Featured Products',
10
+ plural: 'DD Featured Products',
11
+ },
12
+ fields: [
13
+ {
14
+ type: 'tabs',
15
+ tabs: [
16
+ {
17
+ label: 'Header',
18
+ fields: [
19
+ {
20
+ name: 'eyebrow',
21
+ type: 'text',
22
+ label: 'Eyebrow Label',
23
+ defaultValue: 'What We Supply',
24
+ admin: {
25
+ description: 'Optional small label above the heading',
26
+ placeholder: 'What We Supply',
27
+ },
28
+ },
29
+ {
30
+ name: 'heading',
31
+ type: 'text',
32
+ label: 'Heading',
33
+ defaultValue: 'Our Product Range',
34
+ admin: {
35
+ placeholder: 'Our Product Range',
36
+ },
37
+ },
38
+ {
39
+ name: 'description',
40
+ type: 'textarea',
41
+ label: 'Description',
42
+ defaultValue:
43
+ 'Industry-leading plastic sheet materials for every application — from signage to industrial packaging.',
44
+ admin: {
45
+ placeholder:
46
+ 'Industry-leading plastic sheet materials for every application — from signage to industrial packaging.',
47
+ },
48
+ },
49
+
50
+ link({
51
+ appearances: false,
52
+ overrides: {
53
+ name: 'viewAllUrl',
54
+ label: 'View All Link',
55
+ },
56
+ }),
57
+ ],
58
+ },
59
+ {
60
+ label: 'Products',
61
+ fields: [
62
+ {
63
+ name: 'products',
64
+ type: 'array',
65
+ label: 'Products',
66
+ minRows: 1,
67
+ labels: {
68
+ singular: 'Product',
69
+ plural: 'Products',
70
+ },
71
+ fields: [
72
+ {
73
+ name: 'name',
74
+ type: 'text',
75
+ label: 'Product Name',
76
+ required: true,
77
+ },
78
+ {
79
+ name: 'slug',
80
+ type: 'text',
81
+ label: 'Product Slug',
82
+ required: true,
83
+ admin: {
84
+ placeholder: 'acrylic-sheets',
85
+ description: 'Used for the product detail URL e.g. /products/acrylic-sheets',
86
+ },
87
+ },
88
+ {
89
+ name: 'category',
90
+ type: 'text',
91
+ label: 'Category',
92
+ admin: {
93
+ placeholder: 'Acrylic',
94
+ },
95
+ },
96
+ {
97
+ name: 'shortDesc',
98
+ type: 'textarea',
99
+ label: 'Short Description',
100
+ },
101
+ {
102
+ name: 'badge',
103
+ type: 'text',
104
+ label: 'Badge',
105
+ admin: {
106
+ placeholder: 'Popular',
107
+ },
108
+ },
109
+ {
110
+ name: 'image',
111
+ type: 'upload',
112
+ label: 'Product Image',
113
+ relationTo: 'media',
114
+ },
115
+ {
116
+ name: 'imageFallbackUrl',
117
+ type: 'text',
118
+ label: 'Image Fallback URL',
119
+ admin: {
120
+ placeholder: '/assets/placeholder/product.png',
121
+ description: 'Used when no uploaded image is set',
122
+ },
123
+ },
124
+ {
125
+ name: 'features',
126
+ type: 'array',
127
+ label: 'Feature Tags',
128
+ maxRows: 4,
129
+ labels: {
130
+ singular: 'Feature',
131
+ plural: 'Features',
132
+ },
133
+ fields: [
134
+ {
135
+ name: 'feature',
136
+ type: 'text',
137
+ label: 'Feature',
138
+ required: true,
139
+ },
140
+ ],
141
+ },
142
+ ],
143
+ },
144
+ ],
145
+ },
146
+ {
147
+ label: 'Settings',
148
+ fields: [
149
+ {
150
+ name: 'sectionId',
151
+ type: 'text',
152
+ label: 'Section ID',
153
+ admin: {
154
+ description: 'Optional ID for anchor linking',
155
+ placeholder: 'featured-products',
156
+ },
157
+ },
158
+ {
159
+ name: 'customCSS',
160
+ type: 'code',
161
+ label: 'Custom CSS',
162
+ admin: {
163
+ language: 'css',
164
+ },
165
+ },
166
+ ],
167
+ },
168
+ ],
169
+ },
170
+ ],
171
+ }
172
+
173
+ export default DDFeatProd