Skip to content

Commit 02d6018

Browse files
nikoshellegeakmanclytaemnestra
authored
Update speakers page look. (#1159)
Co-authored-by: Ege Akman <[email protected]> Co-authored-by: Mia Bajić <[email protected]>
1 parent 885fbc9 commit 02d6018

File tree

3 files changed

+318
-73
lines changed

3 files changed

+318
-73
lines changed
+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
---
2+
import { getCollection } from "astro:content";
3+
import type { CollectionEntry } from "astro:content";
4+
import { Image } from "astro:assets";
5+
6+
const allSpeakers = await getCollection("speakers");
7+
8+
const validSpeakers = allSpeakers.filter(speaker =>
9+
!!speaker.data?.name && !!speaker.data?.avatar
10+
);
11+
12+
function getRandomSpeakers(speakers: CollectionEntry<"speakers">[], count: number) {
13+
const shuffled = [...speakers].sort(() => 0.5 - Math.random());
14+
return shuffled.slice(0, Math.min(count, speakers.length));
15+
}
16+
17+
const featuredSpeakers = getRandomSpeakers(validSpeakers, 15);
18+
19+
const sectionTitle = "Featured Speakers";
20+
const sectionSubtitle = "Meet some of our amazing speakers";
21+
22+
---
23+
24+
<section class="py-16 px-4 bg-gray-50">
25+
<div class="container mx-auto">
26+
<div class="text-center mb-12">
27+
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{sectionTitle}</h2>
28+
<p class="text-lg text-gray-600">{sectionSubtitle}</p>
29+
</div>
30+
31+
<div class="speakers-carousel-container relative overflow-hidden">
32+
<div class="speakers-track flex transition-transform duration-1000 ease-linear">
33+
{[...featuredSpeakers, ...featuredSpeakers].map((speaker, _index) => (
34+
<div class="speaker-slide w-full sm:w-1/2 md:w-1/3 lg:w-1/5 flex-shrink-0 px-3">
35+
<a href={`/speaker/${speaker.id}`} class="block group">
36+
<div class="bg-white rounded-lg shadow-md overflow-hidden transition-transform duration-300 group-hover:-translate-y-2">
37+
<div class="aspect-square overflow-hidden">
38+
<Image
39+
src={speaker.data.avatar}
40+
alt={`${speaker.data.name}'s profile picture`}
41+
class="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
42+
height={250}
43+
width={250}
44+
loading="eager"
45+
/>
46+
</div>
47+
<div class="p-4 text-center h-[88px] flex flex-col justify-center">
48+
<!-- Solution: Use text-ellipsis with 2 lines max and remove whitespace-nowrap -->
49+
<h3 class="font-bold text-lg text-gray-900 group-hover:text-primary transition-colors line-clamp-2">
50+
{speaker.data.name}
51+
</h3>
52+
</div>
53+
</div>
54+
</a>
55+
</div>
56+
))}
57+
</div>
58+
</div>
59+
60+
<div class="text-center mt-10">
61+
<a
62+
href="/speakers"
63+
class="inline-block px-6 py-3 bg-primary hover:bg-primary-dark text-white font-medium rounded-md transition-colors"
64+
>
65+
See all speakers
66+
</a>
67+
</div>
68+
</div>
69+
</section>
70+
71+
<script>
72+
document.addEventListener('DOMContentLoaded', () => {
73+
const track = document.querySelector('.speakers-track') as HTMLElement;
74+
const slides = document.querySelectorAll('.speaker-slide');
75+
const totalOriginalSlides = slides.length / 2;
76+
77+
if (!track || slides.length === 0) return;
78+
79+
let currentPosition = 0;
80+
const scrollSpeed = 4000;
81+
let slidingInterval: ReturnType<typeof setInterval> | null = null;
82+
83+
// Function to determine slides per view based on window width
84+
function getSlidesPerView() {
85+
const width = window.innerWidth;
86+
if (width <= 480) return 1;
87+
if (width <= 640) return 2;
88+
if (width <= 768) return 3;
89+
if (width <= 1024) return 4;
90+
return 5;
91+
}
92+
93+
function updateCarouselSettings() {
94+
const slidesPerView = getSlidesPerView();
95+
const slideWidth = 100 / slidesPerView;
96+
97+
// Reset position when breakpoint changes
98+
currentPosition = 0;
99+
track.style.transition = 'none';
100+
track.style.transform = `translateX(0)`;
101+
102+
// Clear and restart animation
103+
if (slidingInterval !== null) {
104+
clearInterval(slidingInterval);
105+
}
106+
startAnimation(slideWidth);
107+
}
108+
109+
function startAnimation(slideWidth: number) {
110+
// Initial setup
111+
moveCarousel();
112+
slidingInterval = setInterval(moveCarousel, scrollSpeed);
113+
114+
function moveCarousel() {
115+
currentPosition += slideWidth;
116+
117+
track.style.transition = 'transform 1000ms linear';
118+
track.style.transform = `translateX(-${currentPosition}%)`;
119+
120+
// Reset when we've scrolled through the original set of slides
121+
if (currentPosition >= slideWidth * totalOriginalSlides) {
122+
setTimeout(() => {
123+
track.style.transition = 'none';
124+
currentPosition = 0;
125+
track.style.transform = `translateX(0)`;
126+
127+
setTimeout(() => {
128+
track.style.transition = 'transform 1000ms linear';
129+
}, 20);
130+
}, 1000);
131+
}
132+
}
133+
}
134+
135+
// Start the carousel
136+
updateCarouselSettings();
137+
138+
// Update on window resize
139+
window.addEventListener('resize', () => {
140+
updateCarouselSettings();
141+
});
142+
143+
// Pause on hover
144+
const carouselContainer = document.querySelector('.speakers-carousel-container');
145+
carouselContainer?.addEventListener('mouseenter', () => {
146+
if (slidingInterval !== null) {
147+
clearInterval(slidingInterval);
148+
slidingInterval = null;
149+
}
150+
});
151+
152+
carouselContainer?.addEventListener('mouseleave', () => {
153+
updateCarouselSettings();
154+
});
155+
156+
// Pause when tab is not visible
157+
document.addEventListener('visibilitychange', () => {
158+
if (document.hidden) {
159+
if (slidingInterval !== null) {
160+
clearInterval(slidingInterval);
161+
slidingInterval = null;
162+
}
163+
} else {
164+
updateCarouselSettings();
165+
}
166+
});
167+
});
168+
</script>
169+
170+
<style>
171+
.speakers-carousel-container {
172+
position: relative;
173+
width: 100%;
174+
}
175+
176+
.speakers-track {
177+
will-change: transform;
178+
}
179+
180+
/* Line clamping for text overflow */
181+
.line-clamp-1 {
182+
display: -webkit-box;
183+
-webkit-line-clamp: 1;
184+
-webkit-box-orient: vertical;
185+
overflow: hidden;
186+
}
187+
188+
.line-clamp-2 {
189+
display: -webkit-box;
190+
-webkit-line-clamp: 2;
191+
-webkit-box-orient: vertical;
192+
overflow: hidden;
193+
}
194+
195+
/* These styles match the responsive classes used in the HTML */
196+
@media (max-width: 1024px) {
197+
.speaker-slide {
198+
width: 25%;
199+
}
200+
}
201+
202+
@media (max-width: 768px) {
203+
.speaker-slide {
204+
width: 33.333%;
205+
}
206+
}
207+
208+
@media (max-width: 640px) {
209+
.speaker-slide {
210+
width: 50%;
211+
}
212+
}
213+
214+
@media (max-width: 480px) {
215+
.speaker-slide {
216+
width: 100%;
217+
}
218+
}
219+
</style>

src/pages/index.astro

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Updates from "@sections/updates.astro";
88
import Prague from "@sections/prague.astro";
99
import Sponsors from "@components/sponsors/sponsors.astro";
1010
import Subscribe from "@sections/subscribe.astro";
11+
import Speakers from "@sections/speakers.astro";
1112
1213
let deadlines = await getCollection("deadlines");
1314
deadlines = deadlines
@@ -23,6 +24,7 @@ deadlines = deadlines
2324
<Hero />
2425
<Updates />
2526
<Keynoters />
27+
<Speakers />
2628
<Prague />
2729
<Sponsors />
2830
<Subscribe />

0 commit comments

Comments
 (0)