From a3437a3d25818fe8bc2715a9bea570a257a03ddf Mon Sep 17 00:00:00 2001 From: Antonio Jimenez Date: Sat, 25 Jul 2026 00:03:04 +0200 Subject: [PATCH] Add homepage announcement banner for Typelevel Traverses Europe --- src/README.md | 4 + src/announcements/announcement.css | 236 ++++++++++++++++++ src/announcements/announcement.js | 60 +++++ .../traverses-europe-2026.template.html | 86 +++++++ src/templates/main.template.html | 9 + 5 files changed, 395 insertions(+) create mode 100644 src/announcements/announcement.css create mode 100644 src/announcements/announcement.js create mode 100644 src/announcements/traverses-europe-2026.template.html diff --git a/src/README.md b/src/README.md index df9e5ad1..ec0dc937 100644 --- a/src/README.md +++ b/src/README.md @@ -1,6 +1,10 @@ {% laika.title = Typelevel laika.html.template = templates/home.template.html + homepage-announcement { + enabled = true + template = "/announcements/traverses-europe-2026.template.html" + } %} This is a placeholder for the landing page. No content from this markdown file is rendered. diff --git a/src/announcements/announcement.css b/src/announcements/announcement.css new file mode 100644 index 00000000..94696873 --- /dev/null +++ b/src/announcements/announcement.css @@ -0,0 +1,236 @@ +.homepage-announcement { + background: + linear-gradient(115deg, hsl(273deg 39% 29%), hsl(273deg 36% 47%) 58%, hsl(288deg 42% 34%)); + border-bottom: 1px solid hsl(0deg 0% 0% / 12%); + color: var(--bulma-white); + isolation: isolate; + overflow: hidden; + position: relative; +} + +.homepage-announcement::before { + background: url("../img/favicon.svg") center / contain no-repeat; + content: ""; + height: 34rem; + left: 52%; + opacity: 0.13; + pointer-events: none; + position: absolute; + top: 50%; + transform: translate(-50%, -50%) rotate(-7deg); + width: 34rem; + z-index: -1; +} + +.homepage-announcement::after { + background: hsl(0deg 0% 100% / 5%); + clip-path: polygon(34% 0, 100% 0, 66% 100%, 0 100%); + content: ""; + inset: 0 18% 0 38%; + pointer-events: none; + position: absolute; + z-index: -1; +} + +.homepage-announcement [hidden] { + display: none !important; +} + +.homepage-announcement__inner { + align-items: center; + display: grid; + gap: 1.75rem; + grid-template-areas: "action copy countdown"; + grid-template-columns: auto minmax(0, 1fr) auto; + min-height: 9.25rem; + padding: 1.15rem 1.5rem; + width: 100%; +} + +.homepage-announcement__copy { + grid-area: copy; + min-width: 0; +} + +.homepage-announcement__eyebrow, +.homepage-announcement__title, +.homepage-announcement__countdown-label { + color: inherit; + margin: 0; +} + +.homepage-announcement__eyebrow { + font-size: 0.68rem; + font-weight: var(--bulma-weight-semibold); + letter-spacing: 0.08em; + margin-bottom: 0.25rem; + text-transform: uppercase; +} + +.homepage-announcement__title { + font-size: 1.35rem; + font-weight: var(--bulma-weight-bold); + line-height: 1.2; +} + +.homepage-announcement__events { + display: flex; + flex-wrap: wrap; + font-size: 0.82rem; + gap: 0.2rem 0.65rem; + list-style: none; + margin: 0.35rem 0 0; + padding: 0; +} + +.homepage-announcement__events li { + align-items: baseline; + display: flex; + white-space: nowrap; +} + +.homepage-announcement__events a { + color: inherit; + display: flex; + gap: 0.3rem; + text-decoration-color: hsl(0deg 0% 100% / 45%); + text-decoration-line: underline; + text-underline-offset: 0.18em; +} + +.homepage-announcement__events a:hover { + color: inherit; + text-decoration-color: currentColor; +} + +.homepage-announcement__events a:focus-visible { + border-radius: 2px; + outline: 2px solid var(--bulma-white); + outline-offset: 3px; +} + +.homepage-announcement__events li:not(:last-child)::after { + content: "·"; + margin-left: 0.35rem; + opacity: 0.65; +} + +.homepage-announcement__events time { + opacity: 0.78; +} + +.homepage-announcement__action { + align-items: center; + background: hsl(0deg 0% 100% / 6%); + border: 2px solid hsl(0deg 0% 100% / 88%); + border-radius: var(--bulma-radius); + color: inherit; + display: inline-flex; + font-size: 0.9rem; + font-weight: var(--bulma-weight-semibold); + gap: 0.45rem; + grid-area: action; + justify-content: center; + min-height: 2.75rem; + padding: 0.55rem 0.9rem; + text-decoration: none; + transition: background-color 150ms ease, color 150ms ease; + white-space: nowrap; +} + +.homepage-announcement__action:hover { + background: var(--bulma-white); + color: hsl(273deg 39% 29%); + text-decoration: none; +} + +.homepage-announcement__action:focus-visible { + outline: 3px solid var(--bulma-white); + outline-offset: 3px; +} + +.homepage-announcement__countdown { + grid-area: countdown; + min-width: 10.75rem; + text-align: center; +} + +.homepage-announcement__countdown-label { + font-size: 0.62rem; + font-weight: var(--bulma-weight-semibold); + letter-spacing: 0.08em; + margin-bottom: 0.4rem; + opacity: 0.82; + text-transform: uppercase; +} + +.homepage-announcement__countdown-units { + display: flex; + font-variant-numeric: tabular-nums; + gap: 0.9rem; + justify-content: center; +} + +.homepage-announcement__countdown-units span { + min-width: 2.5rem; +} + +.homepage-announcement__countdown-units strong, +.homepage-announcement__countdown-units small { + color: inherit; + display: block; +} + +.homepage-announcement__countdown-units strong { + font-size: 1.85rem; + line-height: 1; +} + +.homepage-announcement__countdown-units small { + font-size: 0.56rem; + letter-spacing: 0.05em; + margin-top: 0.25rem; + opacity: 0.76; + text-transform: uppercase; +} + +@media screen and (max-width: 768px) { + .homepage-announcement::before { + height: 30rem; + left: 78%; + opacity: 0.11; + width: 30rem; + } + + .homepage-announcement::after { + inset-inline: 8% 42%; + } + + .homepage-announcement__inner { + gap: 0.9rem; + grid-template-areas: + "copy" + "action" + "countdown"; + grid-template-columns: minmax(0, 1fr); + justify-items: center; + min-height: 0; + padding-block: 1.35rem; + text-align: center; + } + + .homepage-announcement__events { + justify-content: center; + } + + .homepage-announcement__action { + min-height: 2.5rem; + padding-block: 0.4rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .homepage-announcement__action { + transition: none; + } +} diff --git a/src/announcements/announcement.js b/src/announcements/announcement.js new file mode 100644 index 00000000..16243bc2 --- /dev/null +++ b/src/announcements/announcement.js @@ -0,0 +1,60 @@ +function initAnnouncementCountdowns() { + document.querySelectorAll("[data-announcement-countdown]").forEach((countdown) => { + const announcement = countdown.closest(".homepage-announcement") || countdown; + const location = countdown.querySelector("[data-announcement-countdown-location]"); + const daysElement = countdown.querySelector("[data-announcement-countdown-days]"); + const hoursElement = countdown.querySelector("[data-announcement-countdown-hours]"); + const minutesElement = countdown.querySelector("[data-announcement-countdown-minutes]"); + const description = countdown.querySelector("[data-announcement-countdown-description]"); + + if ( + [location, daysElement, hoursElement, minutesElement, description].some( + (element) => !element, + ) + ) { + return; + } + + const events = Array.from(announcement.querySelectorAll("[data-announcement-event]")) + .map((event) => ({ + location: event.dataset.eventLocation || "the next event", + start: Date.parse(event.dateTime), + })) + .filter((event) => Number.isFinite(event.start)) + .sort((first, second) => first.start - second.start); + let interval; + + const formatUnit = (value, unit) => + `${value} ${unit}${value === 1 ? "" : "s"}`; + + function updateCountdown() { + const now = Date.now(); + const nextEvent = events.find((event) => event.start >= now); + + if (!nextEvent) { + countdown.hidden = true; + if (interval) window.clearInterval(interval); + return; + } + + const totalMinutes = Math.max(0, Math.floor((nextEvent.start - now) / 60000)); + const days = Math.floor(totalMinutes / 1440); + const hours = Math.floor((totalMinutes % 1440) / 60); + const minutes = totalMinutes % 60; + + location.textContent = nextEvent.location; + daysElement.textContent = days; + hoursElement.textContent = hours; + minutesElement.textContent = minutes; + description.textContent = + `${formatUnit(days, "day")}, ${formatUnit(hours, "hour")}, and ` + + `${formatUnit(minutes, "minute")} until the ${nextEvent.location} event.`; + countdown.hidden = false; + } + + updateCountdown(); + if (!countdown.hidden) interval = window.setInterval(updateCountdown, 60000); + }); +} + +initAnnouncementCountdowns(); diff --git a/src/announcements/traverses-europe-2026.template.html b/src/announcements/traverses-europe-2026.template.html new file mode 100644 index 00000000..6aa30373 --- /dev/null +++ b/src/announcements/traverses-europe-2026.template.html @@ -0,0 +1,86 @@ +
+
+
+

Three meetups · October 2026

+

+ Typelevel Traverses Europe +

+ +
+ + + + + Explore events + +
+
diff --git a/src/templates/main.template.html b/src/templates/main.template.html index 183eceb0..04838d09 100644 --- a/src/templates/main.template.html +++ b/src/templates/main.template.html @@ -20,13 +20,22 @@ @:@ + @:if(homepage-announcement.enabled) + + @:@ + @:if(homepage-announcement.enabled) + + @:@ ${cursor.currentDocument.title} + @:if(homepage-announcement.enabled) + @:include(homepage-announcement.template) + @:@ @:include(/templates/nav.template.html)
${_.embeddedBody}