Skip to content

Commit fdea4b8

Browse files
Updated news carousel
1 parent 0425c64 commit fdea4b8

File tree

4 files changed

+260
-942
lines changed

4 files changed

+260
-942
lines changed

_includes/news/_news-carousel.ejs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
```{=html}
2-
<% for (const item of items) { %>
3-
<a href="<%- item.path %>">
4-
<h5><%- item.title %></h5>
5-
<p><%- item['reading-time'] %> min</p>
6-
<% if (item.description) { %>
7-
<p><%- item.description %></p>
8-
<% } %>
9-
<span>
10-
<span><%- item.author %></span>
11-
<span><%- item.date %></span>
12-
</span>
13-
</a>
14-
<% } %>
2+
<div id="news-carousel-container">
3+
<div id="news-carousel-track">
4+
<% for (const item of items) { %>
5+
<div class="news-carousel-slide">
6+
<a href="<%- item.path %>" class="news-carousel-card">
7+
<div class="news-carousel-card-body">
8+
<h5 class="news-carousel-title"><%- item.title %></h5>
9+
<p class="news-carousel-reading-time"><%- item['reading-time'] %> read</p>
10+
<% if (item.description) { %>
11+
<p class="news-carousel-description"><%- item.description %></p>
12+
<% } %>
13+
<div class="news-carousel-attribution">
14+
<span class="news-carousel-author"><%- item.author %></span>
15+
<span class="news-carousel-date"><%- item.date %></span>
16+
</div>
17+
</div>
18+
</a>
19+
</div>
20+
<% } %>
21+
</div>
22+
</div>
1523
```

_includes/news/news.qmd

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,216 @@ listing:
1313
:::
1414

1515
[See all news &rarr;](news/)
16+
17+
```{=html}
18+
<style>
19+
/* Scoped styles for the news carousel to avoid conflicts with global themes */
20+
#news-carousel-container {
21+
overflow: hidden;
22+
position: relative;
23+
width: 100%;
24+
}
25+
26+
#news-carousel-container #news-carousel-track {
27+
display: flex;
28+
align-items: stretch;
29+
}
30+
31+
#news-carousel-container .news-carousel-slide {
32+
flex-shrink: 0;
33+
width: 33.3333%;
34+
padding: 0.5rem;
35+
box-sizing: border-box;
36+
}
37+
38+
@media (max-width: 1024px) {
39+
#news-carousel-container .news-carousel-slide {
40+
width: 50%;
41+
}
42+
}
43+
44+
@media (max-width: 768px) {
45+
#news-carousel-container .news-carousel-slide {
46+
width: 100%;
47+
}
48+
}
49+
50+
#news-carousel-container .news-carousel-card {
51+
overflow: hidden;
52+
display: flex;
53+
flex-direction: column;
54+
height: 100%;
55+
text-decoration: none;
56+
border-radius: 0.5rem;
57+
transition: background-color 0.3s ease, border-color 0.3s ease;
58+
background-color: #f8f9fa;
59+
border: 1px solid #e9ecef;
60+
color: #212529;
61+
}
62+
63+
#news-carousel-container .news-carousel-card:hover {
64+
background-color: #e9ecef;
65+
border-color: #dee2e6;
66+
}
67+
68+
#news-carousel-container .news-carousel-card-body {
69+
flex-grow: 1;
70+
display: flex;
71+
flex-direction: column;
72+
padding: 1rem;
73+
}
74+
75+
#news-carousel-container .news-carousel-title {
76+
white-space: nowrap;
77+
overflow: hidden;
78+
text-overflow: ellipsis;
79+
margin-bottom: 0.25rem;
80+
color: #212529;
81+
font-weight: 700;
82+
}
83+
84+
#news-carousel-container .news-carousel-reading-time,
85+
#news-carousel-container .news-carousel-description,
86+
#news-carousel-container .news-carousel-attribution {
87+
color: #6c757d;
88+
}
89+
90+
#news-carousel-container .news-carousel-reading-time {
91+
font-size: 0.9em;
92+
margin-bottom: 0.75rem;
93+
}
94+
95+
#news-carousel-container .news-carousel-description {
96+
flex-grow: 1;
97+
display: -webkit-box;
98+
-webkit-box-orient: vertical;
99+
-webkit-line-clamp: 2;
100+
line-clamp: 2;
101+
overflow: hidden;
102+
text-overflow: ellipsis;
103+
margin-bottom: 1rem;
104+
}
105+
106+
#news-carousel-container .news-carousel-attribution {
107+
display: flex;
108+
justify-content: space-between;
109+
align-items: flex-end;
110+
gap: 1em;
111+
font-size: 0.85em;
112+
margin-top: auto;
113+
}
114+
115+
#news-carousel-container .news-carousel-author {
116+
white-space: nowrap;
117+
overflow: hidden;
118+
text-overflow: ellipsis;
119+
min-width: 0;
120+
}
121+
122+
#news-carousel-container .news-carousel-date {
123+
white-space: nowrap;
124+
flex-shrink: 0;
125+
}
126+
</style>
127+
128+
<script>
129+
document.addEventListener('DOMContentLoaded', function () {
130+
const carouselContainer = document.getElementById('news-carousel-container');
131+
const carouselTrack = document.getElementById('news-carousel-track');
132+
133+
if (!carouselContainer || !carouselTrack || !carouselTrack.children.length) {
134+
return;
135+
}
136+
137+
const slides = Array.from(carouselTrack.children);
138+
const displayDuration = 2500;
139+
let currentTranslate = 0;
140+
let currentIndex = 0;
141+
let intervalId;
142+
let wheelTimeout;
143+
let isWheeling = false;
144+
145+
const getItemsPerView = () => {
146+
const width = window.innerWidth;
147+
if (width <= 768) return 1;
148+
if (width > 768 && width <= 1024) return 2;
149+
return 3;
150+
}
151+
152+
const startAutoplay = () => {
153+
stopAutoplay();
154+
intervalId = setInterval(autoplayNext, displayDuration);
155+
}
156+
157+
const stopAutoplay = () => {
158+
clearInterval(intervalId);
159+
}
160+
161+
const setSliderPosition = () => {
162+
carouselTrack.style.transform = `translateX(${currentTranslate}px)`;
163+
}
164+
165+
const setPositionByIndex = () => {
166+
if (slides.length === 0) return;
167+
168+
const itemsPerView = getItemsPerView();
169+
const maxIndex = slides.length > itemsPerView ? slides.length - itemsPerView : 0;
170+
171+
if (currentIndex > maxIndex) currentIndex = maxIndex;
172+
if (currentIndex < 0) currentIndex = 0;
173+
174+
const slideWidth = slides[0].getBoundingClientRect().width;
175+
currentTranslate = currentIndex * -slideWidth;
176+
177+
carouselTrack.style.transition = 'transform 0.4s ease-out';
178+
setSliderPosition();
179+
}
180+
181+
const autoplayNext = () => {
182+
if (document.hidden) return;
183+
const itemsPerView = getItemsPerView();
184+
const maxIndex = slides.length > itemsPerView ? slides.length - itemsPerView : 0;
185+
186+
currentIndex++;
187+
if (currentIndex > maxIndex) {
188+
currentIndex = 0;
189+
}
190+
setPositionByIndex();
191+
}
192+
193+
const handleWheel = (event) => {
194+
event.preventDefault();
195+
196+
if (isWheeling) return;
197+
isWheeling = true;
198+
199+
stopAutoplay();
200+
201+
const itemsPerView = getItemsPerView();
202+
const maxIndex = slides.length > itemsPerView ? slides.length - itemsPerView : 0;
203+
const delta = event.deltaY;
204+
205+
if (delta > 0) {
206+
if (currentIndex < maxIndex) currentIndex++;
207+
} else if (delta < 0) {
208+
if (currentIndex > 0) currentIndex--;
209+
}
210+
setPositionByIndex();
211+
212+
clearTimeout(wheelTimeout);
213+
wheelTimeout = setTimeout(startAutoplay, 300);
214+
215+
setTimeout(() => { isWheeling = false; }, 100);
216+
}
217+
218+
carouselContainer.addEventListener('wheel', handleWheel, { passive: false });
219+
carouselContainer.addEventListener('mouseenter', stopAutoplay);
220+
carouselContainer.addEventListener('mouseleave', startAutoplay);
221+
document.addEventListener('visibilitychange', () => document.hidden ? stopAutoplay() : startAutoplay());
222+
window.addEventListener('resize', setPositionByIndex);
223+
224+
setPositionByIndex();
225+
startAutoplay();
226+
});
227+
</script>
228+
```

0 commit comments

Comments
 (0)