Skip to content

Commit d10df19

Browse files
committed
refactor: Use tables for displaying multiple events, not multiple cards
Signed-off-by: Felicitas Pojtinger <felicitas@pojtinger.com>
1 parent 1706875 commit d10df19

2 files changed

Lines changed: 226 additions & 81 deletions

File tree

layouts/_partials/section-events.html

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,32 @@ <h3>Upcoming</h3>
3434
calendar="{{ .Site.Data.links.luma.calendar }}"
3535
proxy="{{ .Site.Data.links.luma.proxy }}"
3636
>
37-
<div class="pf-v6-l-flex pf-m-column pf-m-gap-md">
38-
{{ range (seq 3) }}
39-
<div class="pf-v6-c-card pf-m-compact pf-m-flat">
40-
<div class="pf-v6-c-card__header">
41-
<div class="pf-v6-c-card__header-main">
42-
<div class="pf-v6-l-flex pf-m-gap-lg pf-m-align-items-center pf-m-nowrap">
43-
<div
44-
class="pf-v6-c-skeleton"
45-
style="
46-
--pf-v6-c-skeleton--Height: 4rem;
47-
--pf-v6-c-skeleton--Width: 4rem;
48-
border-radius: var(--pf-t--global--border--radius--medium);
49-
flex-shrink: 0;
50-
"
51-
></div>
52-
<div class="pf-v6-l-flex pf-m-column pf-m-gap-sm pf-m-grow">
53-
<div class="pf-v6-c-skeleton pf-m-text-lg pf-m-width-50"></div>
54-
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75"></div>
55-
</div>
56-
</div>
57-
</div>
58-
</div>
59-
</div>
60-
{{ end }}
61-
</div>
37+
<table
38+
class="pf-v6-c-table pf-m-grid-md pf-m-compact"
39+
role="grid"
40+
aria-label="Upcoming events"
41+
>
42+
<thead class="pf-v6-c-table__thead">
43+
<tr class="pf-v6-c-table__tr" role="row">
44+
<th class="pf-v6-c-table__th" role="columnheader" scope="col"></th>
45+
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Date</th>
46+
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Event</th>
47+
<th class="pf-v6-c-table__th" role="columnheader" scope="col">Location</th>
48+
<th class="pf-v6-c-table__th" role="columnheader" scope="col"></th>
49+
</tr>
50+
</thead>
51+
<tbody class="pf-v6-c-table__tbody" role="rowgroup">
52+
{{ range (seq 3) }}
53+
<tr class="pf-v6-c-table__tr" role="row">
54+
<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>
55+
<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>
56+
<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>
57+
<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>
58+
<td class="pf-v6-c-table__td" role="cell"></td>
59+
</tr>
60+
{{ end }}
61+
</tbody>
62+
</table>
6263
</luma-upcoming-events>
6364
</div>
6465
</section>

static/main.js

Lines changed: 200 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -92,88 +92,232 @@
9292

9393
const eventTemplate = document.getElementById("next-event-template");
9494

95-
const renderEventCard = (evt) => {
95+
const formatEvent = (evt) => {
9696
const start = new Date(evt.start_at);
97-
const now = new Date();
98-
const days = Math.ceil((start - now) / 86400000);
99-
const relative =
100-
days <= 0
101-
? "(today!)"
102-
: days === 1
103-
? "(tomorrow)"
104-
: days < 7
105-
? "(this week)"
106-
: days < 14
107-
? "(next week)"
108-
: `(in ${Math.round(days / 7)} weeks)`;
109-
const time = start.toLocaleTimeString("en-US", {
110-
hour: "numeric",
111-
minute: "2-digit",
112-
});
97+
const days = Math.ceil((start - new Date()) / 86400000);
98+
return {
99+
name: evt.name || "VanLUG Meeting",
100+
date: start.toLocaleDateString("en-US", {
101+
weekday: "short",
102+
month: "short",
103+
day: "numeric",
104+
}),
105+
time: start.toLocaleTimeString("en-US", {
106+
hour: "numeric",
107+
minute: "2-digit",
108+
}),
109+
relative:
110+
days <= 0
111+
? "(today!)"
112+
: days === 1
113+
? "(tomorrow)"
114+
: days < 7
115+
? "(this week)"
116+
: days < 14
117+
? "(next week)"
118+
: `(in ${Math.round(days / 7)} weeks)`,
119+
lumaUrl: evt.url
120+
? `https://luma.com/${encodeURI(evt.url)}`
121+
: "https://luma.com/vanlug",
122+
location: evt.location || "",
123+
coverUrl: evt.cover_url,
124+
};
125+
};
113126

127+
const renderEventCard = (evt) => {
128+
const { name, date, time, relative, lumaUrl, location, coverUrl } =
129+
formatEvent(evt);
114130
const clone = eventTemplate.content.cloneNode(true);
115131
const $ = (s) => clone.querySelector(`[data-slot="${s}"]`);
116132
const cover = $("cover");
117-
if (evt.cover_url) {
118-
cover.src = evt.cover_url;
119-
cover.alt = evt.name || "Event cover";
133+
if (coverUrl) {
134+
cover.src = coverUrl;
135+
cover.alt = name;
120136
} else {
121137
cover.remove();
122138
}
123-
$("date").textContent = start.toLocaleDateString("en-US", {
124-
weekday: "short",
125-
month: "short",
126-
day: "numeric",
127-
});
139+
$("date").textContent = date;
128140
$("relative").textContent = relative;
129-
$("name").textContent = evt.name || "VanLUG Meeting";
130-
$("detail").textContent = evt.location
131-
? `${evt.location} \u00b7 ${time}`
132-
: time;
133-
const link = $("rsvp");
134-
link.href = evt.url
135-
? `https://luma.com/${evt.url}`
136-
: "https://luma.com/vanlug";
141+
$("name").textContent = name;
142+
$("detail").textContent = location ? `${location} \u00b7 ${time}` : time;
143+
$("rsvp").href = lumaUrl;
137144
return clone;
138145
};
139146

147+
const makeCoverImg = (coverUrl, alt) => {
148+
if (!coverUrl) return null;
149+
const img = document.createElement("img");
150+
img.src = coverUrl;
151+
img.alt = alt;
152+
img.style.cssText =
153+
"width: 4rem; height: 4rem; object-fit: cover; border-radius: var(--pf-t--global--border--radius--medium);";
154+
return img;
155+
};
156+
157+
const makeCell = (className, ...children) => {
158+
const td = document.createElement("td");
159+
td.className = className;
160+
td.setAttribute("role", "cell");
161+
td.style.verticalAlign = "middle";
162+
for (const child of children) {
163+
if (child) td.append(child);
164+
}
165+
return td;
166+
};
167+
168+
const renderEventRow = (evt) => {
169+
const { name, date, time, relative, lumaUrl, location, coverUrl } =
170+
formatEvent(evt);
171+
172+
const tr = document.createElement("tr");
173+
tr.className = "pf-v6-c-table__tr";
174+
tr.setAttribute("role", "row");
175+
176+
tr.append(
177+
makeCell(
178+
"pf-v6-c-table__td pf-v6-u-display-none pf-v6-u-display-table-cell-on-md",
179+
makeCoverImg(coverUrl, name),
180+
),
181+
);
182+
183+
const mobileCover = document.createElement("span");
184+
mobileCover.className =
185+
"pf-v6-u-display-inline-block pf-v6-u-display-none-on-md";
186+
const mobileCoverImg = makeCoverImg(coverUrl, name);
187+
if (mobileCoverImg) mobileCover.append(mobileCoverImg);
188+
189+
const mobileName = document.createElement("div");
190+
mobileName.className =
191+
"pf-v6-u-font-weight-bold pf-v6-u-display-block pf-v6-u-display-none-on-md";
192+
mobileName.textContent = name;
193+
194+
const relativeSpan = document.createElement("span");
195+
relativeSpan.className = "pf-v6-u-font-size-xs";
196+
relativeSpan.textContent = relative;
197+
198+
const dateLine = document.createElement("div");
199+
dateLine.append(`${date} `, relativeSpan);
200+
201+
const timeLine = document.createElement("small");
202+
timeLine.className = "pf-v6-u-text-color-subtle";
203+
timeLine.textContent = time;
204+
205+
const textBlock = document.createElement("div");
206+
textBlock.append(mobileName, dateLine, timeLine);
207+
208+
const flex = document.createElement("div");
209+
flex.className = "pf-v6-l-flex pf-m-gap-md pf-m-align-items-center";
210+
flex.append(mobileCover, textBlock);
211+
212+
tr.append(makeCell("pf-v6-c-table__td", flex));
213+
214+
const nameCell = makeCell(
215+
"pf-v6-c-table__td pf-v6-u-display-none pf-v6-u-display-table-cell-on-md",
216+
);
217+
nameCell.textContent = name;
218+
tr.append(nameCell);
219+
220+
const locCell = makeCell("pf-v6-c-table__td");
221+
locCell.textContent = location;
222+
tr.append(locCell);
223+
224+
const rsvpLink = document.createElement("a");
225+
rsvpLink.href = lumaUrl;
226+
rsvpLink.target = "_blank";
227+
rsvpLink.rel = "noopener";
228+
rsvpLink.className = "pf-v6-c-button pf-m-link pf-m-inline pf-m-small";
229+
rsvpLink.append("RSVP", document.createElement("icon-external"));
230+
tr.append(makeCell("pf-v6-c-table__td", rsvpLink));
231+
232+
return tr;
233+
};
234+
140235
customElements.define(
141236
"luma-next-event",
142237
class extends HTMLElement {
143-
connectedCallback() {
238+
async connectedCallback() {
144239
const cal = this.getAttribute("calendar");
145240
const proxy = this.getAttribute("proxy");
146-
fetch(`${proxy}/next-event?calendar=${encodeURIComponent(cal)}`)
147-
.then((r) => r.json())
148-
.then(({ entries }) => {
149-
const evt = entries?.[0];
150-
if (!evt) {
151-
this.textContent =
152-
"No upcoming events right now. Check back soon!";
153-
return;
154-
}
155-
156-
this.replaceChildren();
157-
this.append(renderEventCard(evt));
158-
})
159-
.catch(() => {
160-
this.textContent = "Could not load the next event. ";
161-
const a = document.createElement("a");
162-
a.href = "/events/";
163-
a.textContent = "See all events.";
164-
this.append(a);
165-
});
241+
try {
242+
const { entries } = await fetch(
243+
`${proxy}/next-event?calendar=${encodeURIComponent(cal)}`,
244+
).then((r) => r.json());
245+
const evt = entries?.[0];
246+
if (!evt) {
247+
this.textContent =
248+
"No upcoming events right now. Check back soon!";
249+
return;
250+
}
251+
this.replaceChildren(renderEventCard(evt));
252+
} catch {
253+
this.textContent = "Could not load the next event. ";
254+
const a = document.createElement("a");
255+
a.href = "/events/";
256+
a.textContent = "See all events.";
257+
this.append(a);
258+
}
166259
}
167260
},
168261
);
169262

170263
customElements.define(
171264
"luma-upcoming-events",
172265
class extends HTMLElement {
173-
connectedCallback() {
266+
async connectedCallback() {
174267
const cal = this.getAttribute("calendar");
175268
const proxy = this.getAttribute("proxy");
176-
fetch(`${proxy}/events?calendar=${encodeURIComponent(cal)}`)
269+
try {
270+
const { entries } = await fetch(
271+
`${proxy}/events?calendar=${encodeURIComponent(cal)}`,
272+
).then((r) => r.json());
273+
if (!entries?.length) {
274+
this.textContent =
275+
"No upcoming events right now. Check back soon!";
276+
return;
277+
}
278+
279+
const table = document.createElement("table");
280+
table.className = "pf-v6-c-table pf-m-grid-md pf-m-compact";
281+
table.setAttribute("role", "grid");
282+
table.setAttribute("aria-label", "Upcoming events");
283+
284+
const thead = document.createElement("thead");
285+
thead.className = "pf-v6-c-table__thead";
286+
const headRow = document.createElement("tr");
287+
headRow.className = "pf-v6-c-table__tr";
288+
headRow.setAttribute("role", "row");
289+
for (const label of ["", "Date", "Event", "Location", ""]) {
290+
const th = document.createElement("th");
291+
th.className = "pf-v6-c-table__th";
292+
th.setAttribute("role", "columnheader");
293+
th.setAttribute("scope", "col");
294+
th.textContent = label;
295+
headRow.append(th);
296+
}
297+
thead.append(headRow);
298+
table.append(thead);
299+
300+
const tbody = document.createElement("tbody");
301+
tbody.className = "pf-v6-c-table__tbody";
302+
tbody.setAttribute("role", "rowgroup");
303+
for (const evt of entries) {
304+
tbody.append(renderEventRow(evt));
305+
}
306+
table.append(tbody);
307+
308+
this.replaceChildren(table);
309+
} catch {
310+
this.textContent = "Could not load upcoming events. ";
311+
const a = document.createElement("a");
312+
a.href = "https://luma.com/vanlug";
313+
a.target = "_blank";
314+
a.rel = "noopener";
315+
a.textContent = "View on Luma instead.";
316+
this.append(a);
317+
}
318+
}
319+
},
320+
);
177321
.then((r) => r.json())
178322
.then(({ entries }) => {
179323
if (!entries || entries.length === 0) {

0 commit comments

Comments
 (0)