Skip to content

Commit 39d514a

Browse files
committed
feat: Add Mastodon client in sidebar and use Hugo's bundler
Signed-off-by: Felicitas Pojtinger <felicitas@pojtinger.com>
1 parent 381c6ff commit 39d514a

16 files changed

Lines changed: 555 additions & 360 deletions

assets/js/events.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export const formatEvent = (evt) => {
2+
const start = new Date(evt.start_at);
3+
const days = Math.ceil((start - new Date()) / 86400000);
4+
return {
5+
name: evt.name || "VanLUG Meeting",
6+
date: start.toLocaleDateString("en-CA", {
7+
weekday: "short",
8+
month: "short",
9+
day: "numeric",
10+
}),
11+
time: start.toLocaleTimeString("en-CA", {
12+
hour: "numeric",
13+
minute: "2-digit",
14+
}),
15+
relative:
16+
days <= 0
17+
? "(today!)"
18+
: days === 1
19+
? "(tomorrow)"
20+
: days < 7
21+
? "(this week)"
22+
: days < 14
23+
? "(next week)"
24+
: `(in ${Math.round(days / 7)} weeks)`,
25+
lumaUrl: evt.url
26+
? `https://luma.com/${encodeURI(evt.url)}`
27+
: "https://luma.com/vanlug",
28+
location: evt.location || "",
29+
coverUrl: evt.cover_url,
30+
};
31+
};
32+
33+
export const makeCoverImg = (coverUrl, alt) => {
34+
if (!coverUrl) return null;
35+
const img = document.createElement("img");
36+
img.src = coverUrl;
37+
img.alt = alt;
38+
img.style.cssText =
39+
"width: 4rem; height: 4rem; object-fit: cover; border-radius: var(--pf-t--global--border--radius--medium);";
40+
return img;
41+
};
42+
43+
export const makeCell = (className, ...children) => {
44+
const td = document.createElement("td");
45+
td.className = className;
46+
td.setAttribute("role", "cell");
47+
td.style.verticalAlign = "middle";
48+
for (const child of children) {
49+
if (child) td.append(child);
50+
}
51+
return td;
52+
};

assets/js/icons.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const inButton = (el) => el.closest(".pf-v6-c-button__icon") !== null;
2+
const renderFaIcon = (el, faClasses) => {
3+
const i = `<i class="${faClasses}" aria-hidden="true"></i>`;
4+
if (inButton(el)) {
5+
el.innerHTML = i;
6+
} else {
7+
el.style.display = "inline";
8+
el.style.whiteSpace = "nowrap";
9+
el.innerHTML = "\u2060" + i;
10+
el.querySelector("i").classList.add("pf-v6-u-ml-xs");
11+
}
12+
};
13+
const brandIcons = {
14+
"icon-discourse": "fa-brands fa-discourse",
15+
"icon-signal": "fa-brands fa-signal-messenger",
16+
"icon-mastodon": "fa-brands fa-mastodon",
17+
"icon-bluesky": "fa-brands fa-bluesky",
18+
"icon-linkedin": "fa-brands fa-linkedin",
19+
"icon-paypal": "fa-brands fa-paypal",
20+
};
21+
for (const [tag, cls] of Object.entries(brandIcons)) {
22+
customElements.define(
23+
tag,
24+
class extends HTMLElement {
25+
connectedCallback() {
26+
renderFaIcon(this, cls);
27+
}
28+
},
29+
);
30+
}
31+
customElements.define(
32+
"icon-external",
33+
class extends HTMLElement {
34+
connectedCallback() {
35+
renderFaIcon(this, "fa-solid fa-arrow-up-right-from-square");
36+
}
37+
},
38+
);
39+
customElements.define(
40+
"icon-email",
41+
class extends HTMLElement {
42+
connectedCallback() {
43+
renderFaIcon(this, "fa-solid fa-envelope");
44+
}
45+
},
46+
);
47+
customElements.define(
48+
"icon-custom",
49+
class extends HTMLElement {
50+
connectedCallback() {
51+
renderFaIcon(this, this.dataset.icon);
52+
}
53+
},
54+
);

assets/js/luma-next-event.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { formatEvent } from "./events.js";
2+
3+
const eventTemplate = document.getElementById("next-event-template");
4+
5+
const renderEventCard = (evt) => {
6+
const { name, date, time, relative, lumaUrl, location, coverUrl } =
7+
formatEvent(evt);
8+
const clone = eventTemplate.content.cloneNode(true);
9+
const $ = (s) => clone.querySelector(`[data-slot="${s}"]`);
10+
const cover = $("cover");
11+
if (coverUrl) {
12+
cover.src = coverUrl;
13+
cover.alt = name;
14+
} else {
15+
cover.remove();
16+
}
17+
$("date").textContent = date;
18+
$("relative").textContent = relative;
19+
$("name").textContent = name;
20+
$("detail").textContent = location ? `${location} \u00b7 ${time}` : time;
21+
$("rsvp").href = lumaUrl;
22+
return clone;
23+
};
24+
25+
customElements.define(
26+
"luma-next-event",
27+
class extends HTMLElement {
28+
async connectedCallback() {
29+
const cal = this.getAttribute("calendar");
30+
const proxy = this.getAttribute("proxy");
31+
try {
32+
const { entries } = await fetch(
33+
`${proxy}/next-event?calendar=${encodeURIComponent(cal)}`,
34+
).then((r) => r.json());
35+
const evt = entries?.[0];
36+
if (!evt) {
37+
this.textContent = "No upcoming events right now. Check back soon!";
38+
return;
39+
}
40+
this.replaceChildren(renderEventCard(evt));
41+
} catch {
42+
this.textContent = "Could not load the next event. ";
43+
const a = document.createElement("a");
44+
a.href = "/events/";
45+
a.textContent = "See all events.";
46+
this.append(a);
47+
}
48+
}
49+
},
50+
);

assets/js/luma-upcoming-events.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { formatEvent, makeCoverImg, makeCell } from "./events.js";
2+
3+
const renderEventRow = (evt) => {
4+
const { name, date, time, relative, lumaUrl, location, coverUrl } =
5+
formatEvent(evt);
6+
7+
const tr = document.createElement("tr");
8+
tr.className = "pf-v6-c-table__tr";
9+
tr.setAttribute("role", "row");
10+
11+
tr.append(
12+
makeCell(
13+
"pf-v6-c-table__td pf-v6-u-display-none pf-v6-u-display-table-cell-on-md",
14+
makeCoverImg(coverUrl, name),
15+
),
16+
);
17+
18+
const mobileCover = document.createElement("span");
19+
mobileCover.className =
20+
"pf-v6-u-display-inline-block pf-v6-u-display-none-on-md";
21+
const mobileCoverImg = makeCoverImg(coverUrl, name);
22+
if (mobileCoverImg) mobileCover.append(mobileCoverImg);
23+
24+
const mobileName = document.createElement("div");
25+
mobileName.className =
26+
"pf-v6-u-font-weight-bold pf-v6-u-display-block pf-v6-u-display-none-on-md";
27+
mobileName.textContent = name;
28+
29+
const relativeSpan = document.createElement("span");
30+
relativeSpan.className = "pf-v6-u-font-size-xs";
31+
relativeSpan.textContent = relative;
32+
33+
const dateLine = document.createElement("div");
34+
dateLine.append(`${date} `, relativeSpan);
35+
36+
const timeLine = document.createElement("small");
37+
timeLine.className = "pf-v6-u-text-color-subtle";
38+
timeLine.textContent = time;
39+
40+
const textBlock = document.createElement("div");
41+
textBlock.append(mobileName, dateLine, timeLine);
42+
43+
const flex = document.createElement("div");
44+
flex.className = "pf-v6-l-flex pf-m-gap-md pf-m-align-items-center";
45+
flex.append(mobileCover, textBlock);
46+
47+
tr.append(makeCell("pf-v6-c-table__td", flex));
48+
49+
const nameCell = makeCell(
50+
"pf-v6-c-table__td pf-v6-u-display-none pf-v6-u-display-table-cell-on-md",
51+
);
52+
nameCell.textContent = name;
53+
tr.append(nameCell);
54+
55+
const locCell = makeCell("pf-v6-c-table__td");
56+
locCell.textContent = location;
57+
tr.append(locCell);
58+
59+
const rsvpLink = document.createElement("a");
60+
rsvpLink.href = lumaUrl;
61+
rsvpLink.target = "_blank";
62+
rsvpLink.rel = "noopener";
63+
rsvpLink.className = "pf-v6-c-button pf-m-link pf-m-inline pf-m-small";
64+
rsvpLink.append("RSVP", document.createElement("icon-external"));
65+
tr.append(makeCell("pf-v6-c-table__td", rsvpLink));
66+
67+
return tr;
68+
};
69+
70+
customElements.define(
71+
"luma-upcoming-events",
72+
class extends HTMLElement {
73+
async connectedCallback() {
74+
const cal = this.getAttribute("calendar");
75+
const proxy = this.getAttribute("proxy");
76+
try {
77+
const { entries } = await fetch(
78+
`${proxy}/events?calendar=${encodeURIComponent(cal)}`,
79+
).then((r) => r.json());
80+
if (!entries?.length) {
81+
this.textContent = "No upcoming events right now. Check back soon!";
82+
return;
83+
}
84+
85+
const table = document.createElement("table");
86+
table.className = "pf-v6-c-table pf-m-grid-md pf-m-compact";
87+
table.setAttribute("role", "grid");
88+
table.setAttribute("aria-label", "Upcoming events");
89+
90+
const thead = document.createElement("thead");
91+
thead.className = "pf-v6-c-table__thead";
92+
const headRow = document.createElement("tr");
93+
headRow.className = "pf-v6-c-table__tr";
94+
headRow.setAttribute("role", "row");
95+
for (const label of ["", "Date", "Event", "Location", ""]) {
96+
const th = document.createElement("th");
97+
th.className = "pf-v6-c-table__th";
98+
th.setAttribute("role", "columnheader");
99+
th.setAttribute("scope", "col");
100+
th.textContent = label;
101+
headRow.append(th);
102+
}
103+
thead.append(headRow);
104+
table.append(thead);
105+
106+
const tbody = document.createElement("tbody");
107+
tbody.className = "pf-v6-c-table__tbody";
108+
tbody.setAttribute("role", "rowgroup");
109+
for (const evt of entries) {
110+
tbody.append(renderEventRow(evt));
111+
}
112+
table.append(tbody);
113+
114+
this.replaceChildren(table);
115+
} catch {
116+
this.textContent = "Could not load upcoming events. ";
117+
const a = document.createElement("a");
118+
a.href = "https://luma.com/vanlug";
119+
a.target = "_blank";
120+
a.rel = "noopener";
121+
a.textContent = "View on Luma instead.";
122+
this.append(a);
123+
}
124+
}
125+
},
126+
);

assets/js/main.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import "./theme.js";
2+
import "./sidebar.js";
3+
import "./icons.js";
4+
import "./luma-next-event.js";
5+
import "./luma-upcoming-events.js";
6+
import "./mastodon-feed.js";

0 commit comments

Comments
 (0)