Skip to content

Commit 5c50b36

Browse files
committed
feat: add CarouselBlock support to Puck configuration
- Integrated CarouselBlockConfig and CarouselBlockProps into the Puck configuration. - Updated media category components list to include CarouselBlock for enhanced layout options.
1 parent a879a39 commit 5c50b36

File tree

11 files changed

+1392
-1
lines changed

11 files changed

+1392
-1
lines changed

src/src/app/test-carousel/page.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client'
2+
3+
import React from 'react'
4+
import { CarouselBlock } from '@/components/puck/components/CarouselBlock'
5+
import type { CarouselBlockProps } from '@/components/puck/components/CarouselBlock/CarouselBlockProps'
6+
7+
export default function TestCarouselPage() {
8+
const testCarouselProps: CarouselBlockProps = {
9+
slides: [
10+
{
11+
id: '1',
12+
imageUrl: 'https://picsum.photos/1200/600?random=1',
13+
title: 'Welcome to Our Platform',
14+
description: 'Discover amazing features and possibilities with our innovative solution.',
15+
buttonText: 'Get Started',
16+
buttonUrl: '#',
17+
backgroundColor: '#000000',
18+
textColor: '#ffffff',
19+
textAlignment: 'center',
20+
overlayOpacity: 0.3,
21+
},
22+
{
23+
id: '2',
24+
imageUrl: 'https://picsum.photos/1200/600?random=2',
25+
title: 'Powerful Features',
26+
description: 'Experience the power of our advanced features designed for modern needs.',
27+
buttonText: 'Learn More',
28+
buttonUrl: '#',
29+
backgroundColor: '#1f2937',
30+
textColor: '#ffffff',
31+
textAlignment: 'left',
32+
overlayOpacity: 0.4,
33+
},
34+
{
35+
id: '3',
36+
imageUrl: 'https://picsum.photos/1200/600?random=3',
37+
title: 'Seamless Integration',
38+
description: 'Integrate effortlessly with your existing workflow and tools.',
39+
buttonText: 'Try Now',
40+
buttonUrl: '#',
41+
backgroundColor: '#374151',
42+
textColor: '#ffffff',
43+
textAlignment: 'right',
44+
overlayOpacity: 0.5,
45+
},
46+
],
47+
autoPlay: true,
48+
autoPlayInterval: 5000,
49+
showArrows: true,
50+
showDots: true,
51+
loop: true,
52+
pauseOnHover: true,
53+
height: '400px',
54+
responsive: {
55+
mobile: {
56+
height: '300px',
57+
showArrows: false,
58+
showDots: true,
59+
},
60+
tablet: {
61+
height: '350px',
62+
showArrows: true,
63+
showDots: true,
64+
},
65+
desktop: {
66+
height: '400px',
67+
showArrows: true,
68+
showDots: true,
69+
},
70+
},
71+
}
72+
73+
return (
74+
<div className="min-h-screen bg-gray-50 py-8">
75+
<div className="container mx-auto px-4">
76+
<h1 className="text-4xl font-bold text-center mb-8">CarouselBlock Test Page</h1>
77+
78+
<div className="max-w-4xl mx-auto">
79+
<div className="bg-white rounded-lg shadow-lg p-6 mb-8">
80+
<h2 className="text-2xl font-semibold mb-4">Basic Carousel</h2>
81+
<CarouselBlock {...testCarouselProps} />
82+
</div>
83+
84+
<div className="bg-white rounded-lg shadow-lg p-6 mb-8">
85+
<h2 className="text-2xl font-semibold mb-4">Minimal Carousel (No Auto-play)</h2>
86+
<CarouselBlock
87+
{...testCarouselProps}
88+
autoPlay={false}
89+
height="350px"
90+
borderRadius="12px"
91+
shadow={true}
92+
/>
93+
</div>
94+
95+
<div className="bg-white rounded-lg shadow-lg p-6">
96+
<h2 className="text-2xl font-semibold mb-4">Hero Carousel (Large)</h2>
97+
<CarouselBlock
98+
{...testCarouselProps}
99+
height="600px"
100+
autoPlayInterval={7000}
101+
responsive={{
102+
mobile: { height: '400px', showArrows: false },
103+
tablet: { height: '500px' },
104+
desktop: { height: '600px' },
105+
}}
106+
/>
107+
</div>
108+
</div>
109+
</div>
110+
</div>
111+
)
112+
}
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
'use client'
2+
3+
import React, { useState, useEffect, useCallback } from 'react'
4+
import Image from 'next/image'
5+
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from '@/components/ui/carousel'
6+
import { Button } from '@/components/ui/button'
7+
import { CarouselBlockProps, CarouselSlide } from './CarouselBlockProps'
8+
import { CarouselBlockDefaults } from './CarouselBlockDefaults'
9+
10+
export const CarouselBlock = ({
11+
slides = CarouselBlockDefaults.slides,
12+
autoPlay = CarouselBlockDefaults.autoPlay,
13+
autoPlayInterval = CarouselBlockDefaults.autoPlayInterval,
14+
showArrows = CarouselBlockDefaults.showArrows,
15+
showDots = CarouselBlockDefaults.showDots,
16+
loop = CarouselBlockDefaults.loop,
17+
pauseOnHover = CarouselBlockDefaults.pauseOnHover,
18+
height = CarouselBlockDefaults.height,
19+
width = CarouselBlockDefaults.width,
20+
maxWidth = CarouselBlockDefaults.maxWidth,
21+
padding = CarouselBlockDefaults.padding,
22+
borderRadius = CarouselBlockDefaults.borderRadius,
23+
shadow = CarouselBlockDefaults.shadow,
24+
arrowColor = CarouselBlockDefaults.arrowColor,
25+
arrowBackgroundColor = CarouselBlockDefaults.arrowBackgroundColor,
26+
dotColor = CarouselBlockDefaults.dotColor,
27+
dotActiveColor = CarouselBlockDefaults.dotActiveColor,
28+
responsive = CarouselBlockDefaults.responsive,
29+
}: CarouselBlockProps) => {
30+
const [api, setApi] = useState<any>(null)
31+
const [current, setCurrent] = useState(0)
32+
const [isHovered, setIsHovered] = useState(false)
33+
const [isMobile, setIsMobile] = useState(false)
34+
const [isTablet, setIsTablet] = useState(false)
35+
36+
// Responsive detection
37+
useEffect(() => {
38+
const checkScreenSize = () => {
39+
const width = window.innerWidth
40+
setIsMobile(width < 768)
41+
setIsTablet(width >= 768 && width < 1024)
42+
}
43+
44+
checkScreenSize()
45+
window.addEventListener('resize', checkScreenSize)
46+
return () => window.removeEventListener('resize', checkScreenSize)
47+
}, [])
48+
49+
// Auto-play functionality
50+
useEffect(() => {
51+
if (!api || !autoPlay || isHovered) return
52+
53+
const interval = setInterval(() => {
54+
api.scrollNext()
55+
}, autoPlayInterval)
56+
57+
return () => clearInterval(interval)
58+
}, [api, autoPlay, autoPlayInterval, isHovered])
59+
60+
// Update current slide
61+
useEffect(() => {
62+
if (!api) return
63+
64+
api.on('select', () => {
65+
setCurrent(api.selectedScrollSnap())
66+
})
67+
}, [api])
68+
69+
// Get responsive settings
70+
const getResponsiveSettings = () => {
71+
if (isMobile && responsive?.mobile) {
72+
return {
73+
height: responsive.mobile.height || height,
74+
showArrows: responsive.mobile.showArrows ?? showArrows,
75+
showDots: responsive.mobile.showDots ?? showDots,
76+
}
77+
}
78+
if (isTablet && responsive?.tablet) {
79+
return {
80+
height: responsive.tablet.height || height,
81+
showArrows: responsive.tablet.showArrows ?? showArrows,
82+
showDots: responsive.tablet.showDots ?? showDots,
83+
}
84+
}
85+
if (responsive?.desktop) {
86+
return {
87+
height: responsive.desktop.height || height,
88+
showArrows: responsive.desktop.showArrows ?? showArrows,
89+
showDots: responsive.desktop.showDots ?? showDots,
90+
}
91+
}
92+
return { height, showArrows, showDots }
93+
}
94+
95+
const responsiveSettings = getResponsiveSettings()
96+
97+
// Handle dot click
98+
const scrollTo = useCallback((index: number) => {
99+
api?.scrollTo(index)
100+
}, [api])
101+
102+
// Handle mouse events for pause on hover
103+
const handleMouseEnter = useCallback(() => {
104+
if (pauseOnHover) {
105+
setIsHovered(true)
106+
}
107+
}, [pauseOnHover])
108+
109+
const handleMouseLeave = useCallback(() => {
110+
if (pauseOnHover) {
111+
setIsHovered(false)
112+
}
113+
}, [pauseOnHover])
114+
115+
if (!slides || slides.length === 0) {
116+
return (
117+
<div
118+
style={{
119+
height: responsiveSettings.height,
120+
width,
121+
maxWidth,
122+
padding,
123+
borderRadius,
124+
boxShadow: shadow ? '0 10px 25px rgba(0, 0, 0, 0.1)' : 'none',
125+
}}
126+
className="flex items-center justify-center bg-gray-100 text-gray-500"
127+
>
128+
<p>No slides to display</p>
129+
</div>
130+
)
131+
}
132+
133+
return (
134+
<div
135+
style={{
136+
width,
137+
maxWidth,
138+
padding,
139+
borderRadius,
140+
boxShadow: shadow ? '0 10px 25px rgba(0, 0, 0, 0.1)' : 'none',
141+
}}
142+
className="relative"
143+
onMouseEnter={handleMouseEnter}
144+
onMouseLeave={handleMouseLeave}
145+
>
146+
<Carousel
147+
setApi={setApi}
148+
opts={{
149+
loop,
150+
align: 'start',
151+
}}
152+
className="w-full"
153+
>
154+
<CarouselContent>
155+
{slides.map((slide: CarouselSlide, index: number) => (
156+
<CarouselItem key={slide.id || index}>
157+
<div
158+
style={{
159+
height: responsiveSettings.height,
160+
backgroundColor: slide.backgroundColor || '#000000',
161+
position: 'relative',
162+
overflow: 'hidden',
163+
}}
164+
className="relative flex items-center justify-center"
165+
>
166+
{/* Background Image */}
167+
{slide.imageUrl && (
168+
<>
169+
<Image
170+
src={slide.imageUrl}
171+
alt={slide.title || `Slide ${index + 1}`}
172+
fill
173+
style={{ objectFit: 'cover' }}
174+
priority={index === 0}
175+
/>
176+
{/* Overlay */}
177+
<div
178+
style={{
179+
backgroundColor: `rgba(0, 0, 0, ${slide.overlayOpacity || 0.3})`,
180+
position: 'absolute',
181+
top: 0,
182+
left: 0,
183+
right: 0,
184+
bottom: 0,
185+
}}
186+
/>
187+
</>
188+
)}
189+
190+
{/* Content */}
191+
<div
192+
style={{
193+
position: 'relative',
194+
zIndex: 10,
195+
color: slide.textColor || '#ffffff',
196+
textAlign: slide.textAlignment || 'center',
197+
padding: '2rem',
198+
maxWidth: '800px',
199+
width: '100%',
200+
}}
201+
className="flex flex-col items-center justify-center"
202+
>
203+
{slide.title && (
204+
<h2
205+
className="text-3xl md:text-4xl lg:text-5xl font-bold mb-4"
206+
style={{ textAlign: slide.textAlignment || 'center' }}
207+
>
208+
{slide.title}
209+
</h2>
210+
)}
211+
212+
{slide.description && (
213+
<p
214+
className="text-lg md:text-xl mb-6 leading-relaxed"
215+
style={{ textAlign: slide.textAlignment || 'center' }}
216+
>
217+
{slide.description}
218+
</p>
219+
)}
220+
221+
{slide.buttonText && slide.buttonUrl && (
222+
<Button
223+
asChild
224+
size="lg"
225+
className="px-8 py-3 text-lg font-semibold"
226+
style={{
227+
backgroundColor: slide.textColor || '#ffffff',
228+
color: slide.backgroundColor || '#000000',
229+
}}
230+
>
231+
<a href={slide.buttonUrl}>
232+
{slide.buttonText}
233+
</a>
234+
</Button>
235+
)}
236+
</div>
237+
</div>
238+
</CarouselItem>
239+
))}
240+
</CarouselContent>
241+
242+
{/* Navigation Arrows */}
243+
{responsiveSettings.showArrows && slides.length > 1 && (
244+
<>
245+
<CarouselPrevious
246+
style={{
247+
color: arrowColor,
248+
backgroundColor: arrowBackgroundColor,
249+
border: 'none',
250+
}}
251+
className="left-4"
252+
/>
253+
<CarouselNext
254+
style={{
255+
color: arrowColor,
256+
backgroundColor: arrowBackgroundColor,
257+
border: 'none',
258+
}}
259+
className="right-4"
260+
/>
261+
</>
262+
)}
263+
264+
{/* Dots Indicator */}
265+
{responsiveSettings.showDots && slides.length > 1 && (
266+
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex space-x-2">
267+
{slides.map((_, index) => (
268+
<button
269+
key={index}
270+
onClick={() => scrollTo(index)}
271+
className="w-3 h-3 rounded-full transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-white/50"
272+
style={{
273+
backgroundColor: index === current ? dotActiveColor : dotColor,
274+
}}
275+
aria-label={`Go to slide ${index + 1}`}
276+
/>
277+
))}
278+
</div>
279+
)}
280+
</Carousel>
281+
</div>
282+
)
283+
}

0 commit comments

Comments
 (0)