Skip to content

Commit 7bb08d9

Browse files
committed
basic caroulsel component for images inside flexible component
1 parent 37e5417 commit 7bb08d9

File tree

2 files changed

+298
-4
lines changed

2 files changed

+298
-4
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
---
2+
interface Props {
3+
src: string; // Comma-separated image URLs
4+
alt?: string;
5+
interval?: number; // Time between slides in seconds, default 2
6+
autoplay?: boolean; // Whether to auto-advance slides, default true
7+
className?: string;
8+
}
9+
10+
const {
11+
src,
12+
alt = "Carousel image",
13+
interval = 2,
14+
autoplay = true,
15+
className = "",
16+
} = Astro.props;
17+
18+
// Parse comma-separated URLs
19+
const imageUrls = src.split(',').map(url => url.trim()).filter(url => url.length > 0);
20+
21+
// Generate unique ID for this carousel instance
22+
const carouselId = `carousel-${Math.random().toString(36).substr(2, 9)}`;
23+
---
24+
25+
{imageUrls.length === 1 ? (
26+
<!-- Single image fallback -->
27+
<div class={`image-carousel single-image ${className}`}>
28+
<img src={imageUrls[0]} alt={alt} />
29+
</div>
30+
) : (
31+
<!-- Multiple images carousel -->
32+
<div
33+
class={`image-carousel ${className}`}
34+
id={carouselId}
35+
data-interval={interval}
36+
data-autoplay={autoplay}
37+
>
38+
<div class="carousel-container">
39+
<!-- Images -->
40+
<div class="carousel-track">
41+
{imageUrls.map((url, index) => (
42+
<div class={`carousel-slide ${index === 0 ? 'active' : ''}`}>
43+
<img src={url} alt={`${alt} ${index + 1}`} />
44+
</div>
45+
))}
46+
</div>
47+
48+
</div>
49+
50+
<!-- Dots indicator -->
51+
<div class="carousel-dots">
52+
{imageUrls.map((_, index) => (
53+
<button
54+
class={`dot ${index === 0 ? 'active' : ''}`}
55+
data-index={index}
56+
aria-label={`Go to image ${index + 1}`}
57+
></button>
58+
))}
59+
</div>
60+
</div>
61+
)}
62+
63+
<script>
64+
class ImageCarousel {
65+
constructor(element) {
66+
this.element = element;
67+
this.currentIndex = 0;
68+
this.interval = parseInt(element.dataset.interval) * 1000 || 2000;
69+
this.autoplay = element.dataset.autoplay !== 'false'; // Default to true unless explicitly false
70+
this.isPlaying = false;
71+
this.timer = null;
72+
73+
this.slides = element.querySelectorAll('.carousel-slide');
74+
this.dots = element.querySelectorAll('.dot');
75+
76+
console.log('ImageCarousel initialized:', {
77+
slides: this.slides.length,
78+
interval: this.interval,
79+
autoplay: this.autoplay
80+
});
81+
82+
this.init();
83+
}
84+
85+
init() {
86+
// Add event listeners for dots
87+
this.dots.forEach((dot, index) => {
88+
dot.addEventListener('click', () => this.goToSlide(index));
89+
});
90+
91+
// Pause/resume on hover
92+
this.element.addEventListener('mouseenter', () => this.pause());
93+
this.element.addEventListener('mouseleave', () => this.resume());
94+
95+
// Start autoplay if enabled
96+
if (this.autoplay && this.slides.length > 1) {
97+
// Add a small delay to ensure everything is ready
98+
setTimeout(() => {
99+
this.play();
100+
}, 100);
101+
}
102+
}
103+
104+
goToSlide(index) {
105+
// Remove active class from current slide and dot
106+
this.slides[this.currentIndex]?.classList.remove('active');
107+
this.dots[this.currentIndex]?.classList.remove('active');
108+
109+
// Update current index
110+
this.currentIndex = index;
111+
112+
// Add active class to new slide and dot
113+
this.slides[this.currentIndex]?.classList.add('active');
114+
this.dots[this.currentIndex]?.classList.add('active');
115+
}
116+
117+
goToNext() {
118+
const nextIndex = this.currentIndex === this.slides.length - 1 ? 0 : this.currentIndex + 1;
119+
this.goToSlide(nextIndex);
120+
}
121+
122+
goToPrevious() {
123+
const prevIndex = this.currentIndex === 0 ? this.slides.length - 1 : this.currentIndex - 1;
124+
this.goToSlide(prevIndex);
125+
}
126+
127+
play() {
128+
if (this.slides.length <= 1) return;
129+
130+
// Clear any existing timer
131+
this.pause();
132+
133+
console.log('Starting autoplay with interval:', this.interval);
134+
this.timer = setInterval(() => {
135+
console.log('Auto-advancing slide');
136+
this.goToNext();
137+
}, this.interval);
138+
this.isPlaying = true;
139+
}
140+
141+
pause() {
142+
if (this.timer) {
143+
clearInterval(this.timer);
144+
this.timer = null;
145+
}
146+
this.isPlaying = false;
147+
}
148+
149+
resume() {
150+
if (this.autoplay && !this.isPlaying) {
151+
this.play();
152+
}
153+
}
154+
}
155+
156+
// Initialize carousels function
157+
function initializeCarousels() {
158+
const carousels = document.querySelectorAll('.image-carousel:not(.single-image):not([data-initialized])');
159+
console.log('Found carousels to initialize:', carousels.length);
160+
161+
carousels.forEach(carousel => {
162+
carousel.setAttribute('data-initialized', 'true');
163+
new ImageCarousel(carousel);
164+
});
165+
}
166+
167+
// Initialize when DOM is ready
168+
if (document.readyState === 'loading') {
169+
document.addEventListener('DOMContentLoaded', initializeCarousels);
170+
} else {
171+
initializeCarousels();
172+
}
173+
174+
// Also try to initialize after a short delay (for Astro hydration)
175+
setTimeout(initializeCarousels, 500);
176+
</script>
177+
178+
<style lang="scss">
179+
@use "../../styles/mixins" as *;
180+
181+
.image-carousel {
182+
position: relative;
183+
width: 100%;
184+
max-width: 100%;
185+
border-radius: 8px;
186+
overflow: hidden;
187+
188+
// Single image display
189+
&.single-image {
190+
img {
191+
width: 100%;
192+
height: auto;
193+
display: block;
194+
border-radius: 8px;
195+
box-shadow: 0px 12px 24px 0px rgba(0, 0, 0, 0.08);
196+
}
197+
}
198+
199+
.carousel-container {
200+
position: relative;
201+
width: 100%;
202+
overflow: hidden;
203+
border-radius: 8px;
204+
}
205+
206+
.carousel-track {
207+
position: relative;
208+
width: 100%;
209+
height: auto;
210+
}
211+
212+
.carousel-slide {
213+
position: absolute;
214+
top: 0;
215+
left: 0;
216+
width: 100%;
217+
height: 100%;
218+
opacity: 0;
219+
transition: opacity 0.5s ease-in-out;
220+
221+
&.active {
222+
opacity: 1;
223+
position: relative;
224+
}
225+
226+
img {
227+
width: 100%;
228+
height: auto;
229+
display: block;
230+
border-radius: 8px;
231+
box-shadow: 0px 12px 24px 0px rgba(0, 0, 0, 0.08);
232+
}
233+
}
234+
235+
236+
// Dots indicator
237+
.carousel-dots {
238+
display: flex;
239+
justify-content: center;
240+
align-items: center;
241+
gap: 8px;
242+
margin-top: 12px;
243+
padding: 8px 0;
244+
245+
.dot {
246+
width: 10px;
247+
height: 10px;
248+
border-radius: 50%;
249+
border: none;
250+
background: rgba(0, 0, 0, 0.3);
251+
cursor: pointer;
252+
transition: all 0.3s ease;
253+
254+
&:hover {
255+
background: rgba(0, 0, 0, 0.5);
256+
transform: scale(1.1);
257+
}
258+
259+
&.active {
260+
background: var(--primary-button-bg, #0c8ce0);
261+
transform: scale(1.2);
262+
}
263+
}
264+
}
265+
266+
// Responsive adjustments
267+
@include break-down(md) {
268+
.carousel-dots {
269+
margin-top: 10px;
270+
gap: 6px;
271+
272+
.dot {
273+
width: 8px;
274+
height: 8px;
275+
}
276+
}
277+
}
278+
279+
// Dark theme support
280+
.theme-dark & {
281+
.carousel-dots .dot {
282+
background: rgba(255, 255, 255, 0.3);
283+
284+
&:hover {
285+
background: rgba(255, 255, 255, 0.5);
286+
}
287+
288+
&.active {
289+
background: var(--primary-button-bg, #0c8ce0);
290+
}
291+
}
292+
}
293+
}
294+
</style>
295+

src/pages/server.astro

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import api from "/public/images/data/api.png";
1616
import Image from "astro/components/Image.astro";
1717
import YouTubeVideo from "../components/video/YouTubeVideo.astro";
1818
import CTASection from "../components/CTASection.astro";
19+
import ImageCarousel from "../components/image/ImageCarousel.astro";
1920
2021
const testimonialsImportData = await Astro.glob("../data/home/testimonials/**/*.md");
2122
@@ -117,9 +118,7 @@ const sections = [
117118
</div>
118119

119120
<div slot="right">
120-
<Image src={enrollment} alt="Enrollment with Google login" />
121-
<Image src={enrollment} alt="Enrollment with Google login" />
122-
<Image src={enrollment} alt="Enrollment with Google login" />
121+
<ImageCarousel src="/images/data/api.png,/images/data/enrollment-screen.png,/images/data/vision.png" alt="VPN locations dashboard and user details" interval={3} autoplay={true} />
123122
</div>
124123
</FlexibleSection>
125124

@@ -284,7 +283,7 @@ const sections = [
284283
</div>
285284

286285
<div slot="right">
287-
<p>Defguard offers a Yubico YubiKey provisioner — a component that initiates and populates user data on YubiKeys by generating SSH keys as well as GPG/OpenPGP keys. It also stores detailed information about each users key, including the serial number and date of provisioning.</p>
286+
<p>Defguard offers a Yubico YubiKey provisioner — a component that initiates and populates user data on YubiKeys by generating SSH keys as well as GPG/OpenPGP keys. It also stores detailed information about each user's key, including the serial number and date of provisioning.</p>
288287
</div>
289288
</FlexibleSection>
290289

0 commit comments

Comments
 (0)