Skip to content

Commit 7c5e8a3

Browse files
authored
Merge pull request #673 from antoniojimeneznieto/feat/homepage-announcement-banner
Add homepage announcement banner for Typelevel Traverses Europe
2 parents 07bfeca + a3437a3 commit 7c5e8a3

5 files changed

Lines changed: 395 additions & 0 deletions

File tree

src/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
{%
22
laika.title = Typelevel
33
laika.html.template = templates/home.template.html
4+
homepage-announcement {
5+
enabled = true
6+
template = "/announcements/traverses-europe-2026.template.html"
7+
}
48
%}
59

610
This is a placeholder for the landing page. No content from this markdown file is rendered.

src/announcements/announcement.css

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
.homepage-announcement {
2+
background:
3+
linear-gradient(115deg, hsl(273deg 39% 29%), hsl(273deg 36% 47%) 58%, hsl(288deg 42% 34%));
4+
border-bottom: 1px solid hsl(0deg 0% 0% / 12%);
5+
color: var(--bulma-white);
6+
isolation: isolate;
7+
overflow: hidden;
8+
position: relative;
9+
}
10+
11+
.homepage-announcement::before {
12+
background: url("../img/favicon.svg") center / contain no-repeat;
13+
content: "";
14+
height: 34rem;
15+
left: 52%;
16+
opacity: 0.13;
17+
pointer-events: none;
18+
position: absolute;
19+
top: 50%;
20+
transform: translate(-50%, -50%) rotate(-7deg);
21+
width: 34rem;
22+
z-index: -1;
23+
}
24+
25+
.homepage-announcement::after {
26+
background: hsl(0deg 0% 100% / 5%);
27+
clip-path: polygon(34% 0, 100% 0, 66% 100%, 0 100%);
28+
content: "";
29+
inset: 0 18% 0 38%;
30+
pointer-events: none;
31+
position: absolute;
32+
z-index: -1;
33+
}
34+
35+
.homepage-announcement [hidden] {
36+
display: none !important;
37+
}
38+
39+
.homepage-announcement__inner {
40+
align-items: center;
41+
display: grid;
42+
gap: 1.75rem;
43+
grid-template-areas: "action copy countdown";
44+
grid-template-columns: auto minmax(0, 1fr) auto;
45+
min-height: 9.25rem;
46+
padding: 1.15rem 1.5rem;
47+
width: 100%;
48+
}
49+
50+
.homepage-announcement__copy {
51+
grid-area: copy;
52+
min-width: 0;
53+
}
54+
55+
.homepage-announcement__eyebrow,
56+
.homepage-announcement__title,
57+
.homepage-announcement__countdown-label {
58+
color: inherit;
59+
margin: 0;
60+
}
61+
62+
.homepage-announcement__eyebrow {
63+
font-size: 0.68rem;
64+
font-weight: var(--bulma-weight-semibold);
65+
letter-spacing: 0.08em;
66+
margin-bottom: 0.25rem;
67+
text-transform: uppercase;
68+
}
69+
70+
.homepage-announcement__title {
71+
font-size: 1.35rem;
72+
font-weight: var(--bulma-weight-bold);
73+
line-height: 1.2;
74+
}
75+
76+
.homepage-announcement__events {
77+
display: flex;
78+
flex-wrap: wrap;
79+
font-size: 0.82rem;
80+
gap: 0.2rem 0.65rem;
81+
list-style: none;
82+
margin: 0.35rem 0 0;
83+
padding: 0;
84+
}
85+
86+
.homepage-announcement__events li {
87+
align-items: baseline;
88+
display: flex;
89+
white-space: nowrap;
90+
}
91+
92+
.homepage-announcement__events a {
93+
color: inherit;
94+
display: flex;
95+
gap: 0.3rem;
96+
text-decoration-color: hsl(0deg 0% 100% / 45%);
97+
text-decoration-line: underline;
98+
text-underline-offset: 0.18em;
99+
}
100+
101+
.homepage-announcement__events a:hover {
102+
color: inherit;
103+
text-decoration-color: currentColor;
104+
}
105+
106+
.homepage-announcement__events a:focus-visible {
107+
border-radius: 2px;
108+
outline: 2px solid var(--bulma-white);
109+
outline-offset: 3px;
110+
}
111+
112+
.homepage-announcement__events li:not(:last-child)::after {
113+
content: "·";
114+
margin-left: 0.35rem;
115+
opacity: 0.65;
116+
}
117+
118+
.homepage-announcement__events time {
119+
opacity: 0.78;
120+
}
121+
122+
.homepage-announcement__action {
123+
align-items: center;
124+
background: hsl(0deg 0% 100% / 6%);
125+
border: 2px solid hsl(0deg 0% 100% / 88%);
126+
border-radius: var(--bulma-radius);
127+
color: inherit;
128+
display: inline-flex;
129+
font-size: 0.9rem;
130+
font-weight: var(--bulma-weight-semibold);
131+
gap: 0.45rem;
132+
grid-area: action;
133+
justify-content: center;
134+
min-height: 2.75rem;
135+
padding: 0.55rem 0.9rem;
136+
text-decoration: none;
137+
transition: background-color 150ms ease, color 150ms ease;
138+
white-space: nowrap;
139+
}
140+
141+
.homepage-announcement__action:hover {
142+
background: var(--bulma-white);
143+
color: hsl(273deg 39% 29%);
144+
text-decoration: none;
145+
}
146+
147+
.homepage-announcement__action:focus-visible {
148+
outline: 3px solid var(--bulma-white);
149+
outline-offset: 3px;
150+
}
151+
152+
.homepage-announcement__countdown {
153+
grid-area: countdown;
154+
min-width: 10.75rem;
155+
text-align: center;
156+
}
157+
158+
.homepage-announcement__countdown-label {
159+
font-size: 0.62rem;
160+
font-weight: var(--bulma-weight-semibold);
161+
letter-spacing: 0.08em;
162+
margin-bottom: 0.4rem;
163+
opacity: 0.82;
164+
text-transform: uppercase;
165+
}
166+
167+
.homepage-announcement__countdown-units {
168+
display: flex;
169+
font-variant-numeric: tabular-nums;
170+
gap: 0.9rem;
171+
justify-content: center;
172+
}
173+
174+
.homepage-announcement__countdown-units span {
175+
min-width: 2.5rem;
176+
}
177+
178+
.homepage-announcement__countdown-units strong,
179+
.homepage-announcement__countdown-units small {
180+
color: inherit;
181+
display: block;
182+
}
183+
184+
.homepage-announcement__countdown-units strong {
185+
font-size: 1.85rem;
186+
line-height: 1;
187+
}
188+
189+
.homepage-announcement__countdown-units small {
190+
font-size: 0.56rem;
191+
letter-spacing: 0.05em;
192+
margin-top: 0.25rem;
193+
opacity: 0.76;
194+
text-transform: uppercase;
195+
}
196+
197+
@media screen and (max-width: 768px) {
198+
.homepage-announcement::before {
199+
height: 30rem;
200+
left: 78%;
201+
opacity: 0.11;
202+
width: 30rem;
203+
}
204+
205+
.homepage-announcement::after {
206+
inset-inline: 8% 42%;
207+
}
208+
209+
.homepage-announcement__inner {
210+
gap: 0.9rem;
211+
grid-template-areas:
212+
"copy"
213+
"action"
214+
"countdown";
215+
grid-template-columns: minmax(0, 1fr);
216+
justify-items: center;
217+
min-height: 0;
218+
padding-block: 1.35rem;
219+
text-align: center;
220+
}
221+
222+
.homepage-announcement__events {
223+
justify-content: center;
224+
}
225+
226+
.homepage-announcement__action {
227+
min-height: 2.5rem;
228+
padding-block: 0.4rem;
229+
}
230+
}
231+
232+
@media (prefers-reduced-motion: reduce) {
233+
.homepage-announcement__action {
234+
transition: none;
235+
}
236+
}

src/announcements/announcement.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
function initAnnouncementCountdowns() {
2+
document.querySelectorAll("[data-announcement-countdown]").forEach((countdown) => {
3+
const announcement = countdown.closest(".homepage-announcement") || countdown;
4+
const location = countdown.querySelector("[data-announcement-countdown-location]");
5+
const daysElement = countdown.querySelector("[data-announcement-countdown-days]");
6+
const hoursElement = countdown.querySelector("[data-announcement-countdown-hours]");
7+
const minutesElement = countdown.querySelector("[data-announcement-countdown-minutes]");
8+
const description = countdown.querySelector("[data-announcement-countdown-description]");
9+
10+
if (
11+
[location, daysElement, hoursElement, minutesElement, description].some(
12+
(element) => !element,
13+
)
14+
) {
15+
return;
16+
}
17+
18+
const events = Array.from(announcement.querySelectorAll("[data-announcement-event]"))
19+
.map((event) => ({
20+
location: event.dataset.eventLocation || "the next event",
21+
start: Date.parse(event.dateTime),
22+
}))
23+
.filter((event) => Number.isFinite(event.start))
24+
.sort((first, second) => first.start - second.start);
25+
let interval;
26+
27+
const formatUnit = (value, unit) =>
28+
`${value} ${unit}${value === 1 ? "" : "s"}`;
29+
30+
function updateCountdown() {
31+
const now = Date.now();
32+
const nextEvent = events.find((event) => event.start >= now);
33+
34+
if (!nextEvent) {
35+
countdown.hidden = true;
36+
if (interval) window.clearInterval(interval);
37+
return;
38+
}
39+
40+
const totalMinutes = Math.max(0, Math.floor((nextEvent.start - now) / 60000));
41+
const days = Math.floor(totalMinutes / 1440);
42+
const hours = Math.floor((totalMinutes % 1440) / 60);
43+
const minutes = totalMinutes % 60;
44+
45+
location.textContent = nextEvent.location;
46+
daysElement.textContent = days;
47+
hoursElement.textContent = hours;
48+
minutesElement.textContent = minutes;
49+
description.textContent =
50+
`${formatUnit(days, "day")}, ${formatUnit(hours, "hour")}, and ` +
51+
`${formatUnit(minutes, "minute")} until the ${nextEvent.location} event.`;
52+
countdown.hidden = false;
53+
}
54+
55+
updateCountdown();
56+
if (!countdown.hidden) interval = window.setInterval(updateCountdown, 60000);
57+
});
58+
}
59+
60+
initAnnouncementCountdowns();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<section
2+
class="homepage-announcement"
3+
role="region"
4+
aria-labelledby="homepage-announcement-title"
5+
>
6+
<div class="homepage-announcement__inner bulma-container bulma-is-max-desktop">
7+
<div class="homepage-announcement__copy">
8+
<p class="homepage-announcement__eyebrow">Three meetups · October 2026</p>
9+
<p id="homepage-announcement-title" class="homepage-announcement__title">
10+
Typelevel Traverses Europe
11+
</p>
12+
<ul
13+
class="homepage-announcement__events"
14+
aria-label="Event dates and locations"
15+
role="list"
16+
>
17+
<li>
18+
<a href="https://luma.com/iyv37emv">
19+
<span class="bulma-is-sr-only">Register for </span>
20+
<span>Berlin</span>
21+
<time
22+
datetime="2026-10-14T09:00:00+02:00"
23+
data-announcement-event
24+
data-event-location="Berlin"
25+
>14 Oct<span class="bulma-is-sr-only"> 2026</span></time>
26+
</a>
27+
</li>
28+
<li>
29+
<a href="https://luma.com/vzp35rp7">
30+
<span class="bulma-is-sr-only">Register for </span>
31+
<span>London</span>
32+
<time
33+
datetime="2026-10-24T10:00:00+01:00"
34+
data-announcement-event
35+
data-event-location="London"
36+
>24 Oct<span class="bulma-is-sr-only"> 2026</span></time>
37+
</a>
38+
</li>
39+
<li>
40+
<a href="https://luma.com/4s6qxb9p">
41+
<span class="bulma-is-sr-only">Register for </span>
42+
<span>Málaga</span>
43+
<time
44+
datetime="2026-10-28T09:00:00+01:00"
45+
data-announcement-event
46+
data-event-location="Málaga"
47+
>28 Oct<span class="bulma-is-sr-only"> 2026</span></time>
48+
</a>
49+
</li>
50+
</ul>
51+
</div>
52+
53+
<div class="homepage-announcement__countdown" data-announcement-countdown hidden>
54+
<p class="homepage-announcement__countdown-label" aria-hidden="true">
55+
Until <span data-announcement-countdown-location>next event</span>
56+
</p>
57+
<div class="homepage-announcement__countdown-units" aria-hidden="true">
58+
<span>
59+
<strong data-announcement-countdown-days>--</strong>
60+
<small>Days</small>
61+
</span>
62+
<span>
63+
<strong data-announcement-countdown-hours>--</strong>
64+
<small>Hours</small>
65+
</span>
66+
<span>
67+
<strong data-announcement-countdown-minutes>--</strong>
68+
<small>Minutes</small>
69+
</span>
70+
</div>
71+
<p
72+
class="bulma-is-sr-only"
73+
role="timer"
74+
data-announcement-countdown-description
75+
></p>
76+
</div>
77+
78+
<a
79+
class="homepage-announcement__action"
80+
href="@:target(/blog/traverses-europe-2026.md)"
81+
aria-labelledby="homepage-announcement-action homepage-announcement-title"
82+
>
83+
<span id="homepage-announcement-action">Explore events</span>
84+
</a>
85+
</div>
86+
</section>

0 commit comments

Comments
 (0)