Skip to content

Commit 9f09d62

Browse files
committed
feat: Add event types to events page and add interactive tabs to filter between them
Signed-off-by: Felicitas Pojtinger <felicitas@pojtinger.com>
1 parent 6761c27 commit 9f09d62

3 files changed

Lines changed: 244 additions & 63 deletions

File tree

assets/js/luma-upcoming-events.js

Lines changed: 134 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ const renderEventRow = (evt, api) => {
3535
const $ = (s) => clone.querySelector(`[data-slot="${s}"]`);
3636

3737
const desktopCover = makeCoverImg(coverUrl, name);
38-
if (desktopCover) $("cover-desktop").append(desktopCover);
38+
desktopCover && $("cover-desktop").append(desktopCover);
3939

4040
const mobileCover = makeCoverImg(coverUrl, name);
41-
if (mobileCover) $("cover-mobile").append(mobileCover);
41+
mobileCover && $("cover-mobile").append(mobileCover);
4242

4343
$("name-mobile").textContent = name;
4444
$("date").textContent = date;
@@ -47,66 +47,172 @@ const renderEventRow = (evt, api) => {
4747
$("name-desktop").textContent = name;
4848
$("location").textContent = location;
4949
$("rsvp").href = lumaUrl;
50-
$("details").addEventListener("click", () => {
51-
document.querySelector("event-detail-modal").open(apiId, api);
52-
});
50+
$("details").addEventListener("click", () =>
51+
document.querySelector("event-detail-modal").open(apiId, api),
52+
);
53+
54+
clone.querySelector("tr").dataset.eventName = name;
5355

5456
return clone;
5557
};
5658

59+
const makeEmptyState = (html) => {
60+
const el = document.createElement("div");
61+
el.className = "pf-v6-c-empty-state pf-m-xs";
62+
el.innerHTML = `
63+
<div class="pf-v6-c-empty-state__content">
64+
<div class="pf-v6-c-empty-state__header">
65+
<div class="pf-v6-c-empty-state__icon pf-v6-u-font-size-xl pf-v6-u-mb-sm">
66+
<i class="fas fa-calendar" aria-hidden="true"></i>
67+
</div>
68+
<div class="pf-v6-c-empty-state__title">
69+
<p class="pf-v6-c-empty-state__title-text pf-v6-u-font-size-sm">${html}</p>
70+
</div>
71+
</div>
72+
</div>`;
73+
return el;
74+
};
75+
5776
customElements.define(
5877
"luma-upcoming-events",
5978
class extends HTMLElement {
6079
async connectedCallback() {
6180
const cal = this.getAttribute("calendar");
6281
const api = this.getAttribute("api");
82+
const lumaUrl = this.getAttribute("luma") ?? "https://luma.com/vanlug";
83+
const rssUrl = `${api}/events/feed?calendar=${encodeURIComponent(cal)}`;
84+
const subscribeLinks = `<br>Stay updated via <a href="${lumaUrl}" target="_blank" rel="noopener">Luma<icon-external></icon-external></a> or <a href="${rssUrl}" target="_blank" rel="noopener">RSS<icon-custom data-icon="fa-solid fa-rss"></icon-custom></a>.`;
85+
6386
try {
6487
const { entries } = await fetch(
6588
`${api}/events?calendar=${encodeURIComponent(cal)}`,
6689
).then((r) => r.json());
90+
6791
if (!entries?.length) {
68-
this.textContent = "No upcoming events right now. Check back soon!";
92+
this.replaceChildren(
93+
makeEmptyState(`No upcoming events right now.${subscribeLinks}`),
94+
);
6995
return;
7096
}
7197

72-
const table = document.createElement("table");
73-
table.className = "pf-v6-c-table pf-m-grid-md pf-m-compact";
98+
const table = Object.assign(document.createElement("table"), {
99+
className: "pf-v6-c-table pf-m-grid-md pf-m-compact",
100+
});
74101
table.setAttribute("role", "grid");
75102
table.setAttribute("aria-label", "Upcoming events");
76103

77-
const thead = document.createElement("thead");
78-
thead.className = "pf-v6-c-table__thead";
79-
const headRow = document.createElement("tr");
80-
headRow.className = "pf-v6-c-table__tr";
104+
const thead = Object.assign(document.createElement("thead"), {
105+
className: "pf-v6-c-table__thead",
106+
});
107+
const headRow = Object.assign(document.createElement("tr"), {
108+
className: "pf-v6-c-table__tr",
109+
});
81110
headRow.setAttribute("role", "row");
82111
for (const label of ["", "Date", "Event", "Location", ""]) {
83-
const th = document.createElement("th");
84-
th.className = "pf-v6-c-table__th";
112+
const th = Object.assign(document.createElement("th"), {
113+
className: "pf-v6-c-table__th",
114+
textContent: label,
115+
});
85116
th.setAttribute("role", "columnheader");
86117
th.setAttribute("scope", "col");
87-
th.textContent = label;
88118
headRow.append(th);
89119
}
90120
thead.append(headRow);
91121
table.append(thead);
92122

93-
const tbody = document.createElement("tbody");
94-
tbody.className = "pf-v6-c-table__tbody";
123+
const tbody = Object.assign(document.createElement("tbody"), {
124+
className: "pf-v6-c-table__tbody",
125+
});
95126
tbody.setAttribute("role", "rowgroup");
96-
for (const evt of entries) {
97-
tbody.append(renderEventRow(evt, api));
98-
}
127+
for (const evt of entries) tbody.append(renderEventRow(evt, api));
99128
table.append(tbody);
100129

101-
this.replaceChildren(table);
130+
const emptyState = makeEmptyState("");
131+
emptyState.style.display = "none";
132+
const emptyText = emptyState.querySelector(
133+
".pf-v6-c-empty-state__title-text",
134+
);
135+
136+
this.replaceChildren(table, emptyState);
137+
138+
const tabsEl = document.getElementById("event-type-tabs");
139+
if (!tabsEl) return;
140+
141+
const tabs = tabsEl.querySelectorAll(".pf-v6-c-tabs__link");
142+
const rows = () =>
143+
table.querySelectorAll("tbody tr[data-event-name]");
144+
145+
const activate = (tab) => {
146+
for (const t of tabs) {
147+
const isCurrent = t === tab;
148+
t.setAttribute("aria-selected", isCurrent);
149+
t.closest(".pf-v6-c-tabs__item").classList.toggle(
150+
"pf-m-current",
151+
isCurrent,
152+
);
153+
document.getElementById(
154+
t.getAttribute("aria-controls"),
155+
).hidden = !isCurrent;
156+
}
157+
158+
const activePanel = document.getElementById(
159+
tab.getAttribute("aria-controls"),
160+
);
161+
let body = activePanel.querySelector(".pf-v6-c-tab-content__body");
162+
if (!body) {
163+
body = Object.assign(document.createElement("div"), {
164+
className: "pf-v6-c-tab-content__body",
165+
});
166+
activePanel.append(body);
167+
}
168+
body.append(table, emptyState);
169+
170+
const regex = tab.dataset.regex;
171+
let visibleCount = 0;
172+
for (const row of rows()) {
173+
const visible =
174+
!regex || new RegExp(regex, "i").test(row.dataset.eventName);
175+
row.style.display = visible ? "" : "none";
176+
if (visible) visibleCount++;
177+
}
178+
179+
if (visibleCount === 0) {
180+
emptyText.innerHTML =
181+
(tab.dataset.empty ?? "No upcoming events right now.") +
182+
subscribeLinks;
183+
table.style.display = "none";
184+
emptyState.style.display = "";
185+
} else {
186+
table.style.display = "";
187+
emptyState.style.display = "none";
188+
}
189+
};
190+
191+
activate(
192+
tabsEl.querySelector(".pf-v6-c-tabs__link[aria-selected='true']"),
193+
);
194+
195+
tabsEl.addEventListener("click", (e) => {
196+
const tab = e.target.closest(".pf-v6-c-tabs__link");
197+
if (tab) activate(tab);
198+
});
199+
200+
tabsEl.addEventListener("keydown", (e) => {
201+
if (e.key !== "Enter" && e.key !== " ") return;
202+
const tab = e.target.closest(".pf-v6-c-tabs__link");
203+
if (!tab) return;
204+
e.preventDefault();
205+
activate(tab);
206+
});
102207
} catch {
103-
this.textContent = "Could not load upcoming events. ";
104-
const a = document.createElement("a");
105-
a.href = "https://luma.com/vanlug";
106-
a.target = "_blank";
107-
a.rel = "noopener";
108-
a.textContent = "View on Luma instead.";
109-
this.append(a);
208+
this.replaceChildren();
209+
const a = Object.assign(document.createElement("a"), {
210+
href: "https://luma.com/vanlug",
211+
target: "_blank",
212+
rel: "noopener",
213+
textContent: "View on Luma instead.",
214+
});
215+
this.append("Could not load upcoming events. ", a);
110216
}
111217
}
112218
},

data/content.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,35 @@ home:
3636
image: img/cloudsummit-2026.jpg
3737

3838
events:
39+
event_types:
40+
- key: link
41+
title: Linux Link
42+
tagline: Hands-on help at your local library
43+
description: "Drop-in help sessions at local libraries. Bring your laptop, get hands-on help installing Linux, troubleshooting, or learning the command line."
44+
image: design/lee-robinson-Yc9h5SJdEzI-unsplash.jpg
45+
regex: "Linux Link"
46+
empty: "No Linux Link sessions scheduled right now."
47+
- key: talks
48+
title: Linux Talks
49+
tagline: Learn something new every month
50+
description: "Monthly presentations on topics like sysadmin, security, development tools, and career advice. One or two talks each session, followed by Q&A."
51+
image: design/wesley-tingey-bWRX_obQAl8-unsplash.jpg
52+
regex: "Talks"
53+
empty: "No talks scheduled right now."
54+
- key: chill
55+
title: Chill and Hangout
56+
tagline: No agenda, just good company
57+
description: "Casual get-togethers for Linux enthusiasts. No agenda, just friendly conversation, show-and-tell, and community vibes."
58+
image: design/aaron-thomas-dMqlE7lgyOU-unsplash.jpg
59+
regex: "Chill"
60+
empty: "No hangouts scheduled right now."
61+
- key: special
62+
title: Special Events
63+
tagline: Celebrations and one-off gatherings
64+
description: "One-off community events like Hacktoberfest, our Annual General Meeting, the Christmas party, and other celebrations throughout the year."
65+
image: design/albert-stoynov-jooXO8q2Gcs-unsplash.jpg
66+
regex: "Hacktoberfest|Annual General Meeting|AGM|Christmas|Holiday|Foundation Survey"
67+
empty: "No special events coming up right now."
3968
past:
4069
archive_links:
4170
- title: "2011–2012"

layouts/_partials/section-events.html

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ <h1>{{ .Title }}</h1>
1212
</div>
1313
</section>
1414

15+
{{ $et := .Site.Data.content.events.event_types }}
1516
<section
1617
class="pf-v6-c-page__main-section pf-m-limit-width pf-m-align-center"
1718
>
@@ -21,52 +22,97 @@ <h1>{{ .Title }}</h1>
2122
<div class="pf-v6-c-content">
2223
<h3>Upcoming</h3>
2324
<p>
24-
All upcoming events are listed on our
25+
We run {{ range $i, $t := $et }}{{ if $i }}{{ if eq $i (sub (len $et) 1) }}, and {{ else }}, {{ end }}{{ end }}<strong>{{ $t.title }}</strong>{{ end }} events.
26+
Also available as a
2527
<a
2628
href="{{ .Site.Data.links.luma.url }}"
2729
target="_blank"
2830
rel="noopener"
29-
>Luma calendar<icon-external></icon-external></a
30-
>. RSVP there to get reminders, or subscribe via
31+
>calendar on Luma<icon-external></icon-external></a
32+
> and
3133
<a
3234
href="{{ .Site.Data.links.api }}/events/feed?calendar={{ .Site.Data.links.luma.calendar }}"
3335
target="_blank"
3436
rel="noopener"
35-
>RSS<icon-custom data-icon="fa-solid fa-rss"></icon-custom></a
37+
>RSS feed<icon-custom data-icon="fa-solid fa-rss"></icon-custom></a
3638
>.
3739
</p>
3840
</div>
39-
<luma-upcoming-events
40-
calendar="{{ .Site.Data.links.luma.calendar }}"
41-
api="{{ .Site.Data.links.api }}"
42-
>
43-
<table
44-
class="pf-v6-c-table pf-m-grid-md pf-m-compact"
45-
role="grid"
46-
aria-label="Upcoming events"
47-
>
48-
<thead class="pf-v6-c-table__thead">
49-
<tr class="pf-v6-c-table__tr" role="row">
50-
<th class="pf-v6-c-table__th" role="columnheader" scope="col"></th>
51-
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Date</th>
52-
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Event</th>
53-
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Location</th>
54-
<th class="pf-v6-c-table__th" role="columnheader" scope="col"></th>
55-
</tr>
56-
</thead>
57-
<tbody class="pf-v6-c-table__tbody" role="rowgroup">
58-
{{ range (seq 3) }}
59-
<tr class="pf-v6-c-table__tr" role="row">
60-
<td class="pf-v6-c-table__td" role="cell"><div class="pf-v6-c-skeleton" style="--pf-v6-c-skeleton--Height: 4rem; --pf-v6-c-skeleton--Width: 4rem; border-radius: var(--pf-t--global--border--radius--medium);"></div></td>
61-
<td class="pf-v6-c-table__td" role="cell" data-label="Date"><div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75"></div></td>
62-
<td class="pf-v6-c-table__td" role="cell" data-label="Event"><div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50"></div></td>
63-
<td class="pf-v6-c-table__td" role="cell" data-label="Location"><div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75"></div></td>
64-
<td class="pf-v6-c-table__td" role="cell"></td>
65-
</tr>
66-
{{ end }}
67-
</tbody>
68-
</table>
69-
</luma-upcoming-events>
41+
<div class="pf-v6-c-tabs pf-m-fill" id="event-type-tabs" aria-label="Filter by event type">
42+
<ul class="pf-v6-c-tabs__list" role="tablist">
43+
<li class="pf-v6-c-tabs__item pf-m-current" role="presentation">
44+
<button class="pf-v6-c-tabs__link" role="tab" id="event-tab-all" aria-selected="true" aria-controls="event-panel-all" data-regex="" data-empty="No upcoming events right now. Check back soon!">
45+
All
46+
</button>
47+
</li>
48+
{{ range $et }}
49+
<li class="pf-v6-c-tabs__item" role="presentation">
50+
<button class="pf-v6-c-tabs__link" role="tab" id="event-tab-{{ .key }}" aria-selected="false" aria-controls="event-panel-{{ .key }}" data-regex="{{ .regex }}" data-empty="{{ .empty }}">
51+
{{ .title }}
52+
</button>
53+
</li>
54+
{{ end }}
55+
</ul>
56+
</div>
57+
<section class="pf-v6-c-tab-content" role="tabpanel" id="event-panel-all" aria-labelledby="event-tab-all" tabindex="0">
58+
<div class="pf-v6-c-tab-content__body">
59+
<luma-upcoming-events
60+
calendar="{{ .Site.Data.links.luma.calendar }}"
61+
api="{{ .Site.Data.links.api }}"
62+
luma="{{ .Site.Data.links.luma.url }}"
63+
>
64+
<table
65+
class="pf-v6-c-table pf-m-grid-md pf-m-compact"
66+
role="grid"
67+
aria-label="Upcoming events"
68+
>
69+
<thead class="pf-v6-c-table__thead">
70+
<tr class="pf-v6-c-table__tr" role="row">
71+
<th class="pf-v6-c-table__th" role="columnheader" scope="col"></th>
72+
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Date</th>
73+
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Event</th>
74+
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Location</th>
75+
<th class="pf-v6-c-table__th" role="columnheader" scope="col"></th>
76+
</tr>
77+
</thead>
78+
<tbody class="pf-v6-c-table__tbody" role="rowgroup">
79+
{{ range (seq 3) }}
80+
<tr class="pf-v6-c-table__tr" role="row">
81+
<td class="pf-v6-c-table__td" role="cell"><div class="pf-v6-c-skeleton" style="--pf-v6-c-skeleton--Height: 4rem; --pf-v6-c-skeleton--Width: 4rem; border-radius: var(--pf-t--global--border--radius--medium);"></div></td>
82+
<td class="pf-v6-c-table__td" role="cell" data-label="Date"><div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75"></div></td>
83+
<td class="pf-v6-c-table__td" role="cell" data-label="Event"><div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50"></div></td>
84+
<td class="pf-v6-c-table__td" role="cell" data-label="Location"><div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75"></div></td>
85+
<td class="pf-v6-c-table__td" role="cell"></td>
86+
</tr>
87+
{{ end }}
88+
</tbody>
89+
</table>
90+
</luma-upcoming-events>
91+
</div>
92+
</section>
93+
{{ range $et }}
94+
<section class="pf-v6-c-tab-content" role="tabpanel" id="event-panel-{{ .key }}" aria-labelledby="event-tab-{{ .key }}" tabindex="0" hidden>
95+
<div class="pf-v6-c-tab-content__body">
96+
<div
97+
class="pf-v6-c-card pf-m-compact pf-m-plain"
98+
style="
99+
background: url('{{ .image | relURL }}') center / cover;
100+
min-height: 10rem;
101+
"
102+
>
103+
<div class="pf-v6-c-card__body pf-v6-l-flex pf-m-column pf-m-justify-content-flex-end" style="
104+
background: linear-gradient(color-mix(in srgb, var(--pf-t--global--background--color--primary--default) 60%, transparent) 0%, color-mix(in srgb, var(--pf-t--global--background--color--primary--default) 80%, transparent) 50%, var(--pf-t--global--background--color--primary--default) 100%);
105+
border-radius: inherit;
106+
">
107+
<div class="pf-v6-c-content">
108+
<h4>{{ .tagline }}</h4>
109+
<p>{{ .description }}</p>
110+
</div>
111+
</div>
112+
</div>
113+
</div>
114+
</section>
115+
{{ end }}
70116
</div>
71117
</section>
72118

0 commit comments

Comments
 (0)