Skip to content

Commit dd7c3f7

Browse files
committed
feat: Add "details" modal for Luma and event handler API that fetches individual event data
Signed-off-by: Felicitas Pojtinger <felicitas@pojtinger.com>
1 parent c3f7f1a commit dd7c3f7

8 files changed

Lines changed: 487 additions & 97 deletions

File tree

assets/js/event-detail-modal.js

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import DOMPurify from "dompurify";
2+
import { proxyImageUrl } from "./events.js";
3+
4+
const backdropClass = "pf-v6-c-backdrop";
5+
6+
const modalTemplate = document.createElement("template");
7+
modalTemplate.innerHTML = `
8+
<div class="${backdropClass}" style="display: none;">
9+
<div class="pf-v6-l-bullseye">
10+
<div class="pf-v6-c-modal-box pf-m-md" role="dialog" aria-modal="true"
11+
aria-labelledby="event-detail-title" aria-describedby="event-detail-body">
12+
<div class="pf-v6-c-modal-box__close">
13+
<button class="pf-v6-c-button pf-m-plain" type="button" aria-label="Close" data-slot="close">
14+
<span class="pf-v6-c-button__icon">
15+
<i class="fas fa-times" aria-hidden="true"></i>
16+
</span>
17+
</button>
18+
</div>
19+
<header class="pf-v6-c-modal-box__header">
20+
<div class="pf-v6-c-modal-box__header-main">
21+
<h1 class="pf-v6-c-modal-box__title" id="event-detail-title" data-slot="title"></h1>
22+
</div>
23+
</header>
24+
<div class="pf-v6-c-modal-box__body" id="event-detail-body" tabindex="0" data-slot="body">
25+
<div data-slot="content">
26+
<div class="pf-v6-c-skeleton pf-v6-u-mb-md" style="--pf-v6-c-skeleton--Height: 10rem; border-radius: var(--pf-t--global--border--radius--medium);"></div>
27+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75 pf-v6-u-mb-sm"></div>
28+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50 pf-v6-u-mb-sm"></div>
29+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75 pf-v6-u-mb-sm"></div>
30+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50 pf-v6-u-mb-md"></div>
31+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-v6-u-mb-sm"></div>
32+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75 pf-v6-u-mb-sm"></div>
33+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50"></div>
34+
</div>
35+
</div>
36+
<footer class="pf-v6-c-modal-box__footer">
37+
<a data-slot="rsvp" target="_blank" rel="noopener"
38+
class="pf-v6-c-button pf-m-primary">RSVP on Luma</a>
39+
<button data-slot="cancel" type="button"
40+
class="pf-v6-c-button pf-m-link">Close</button>
41+
</footer>
42+
</div>
43+
</div>
44+
</div>`;
45+
46+
const detailTemplate = document.createElement("template");
47+
detailTemplate.innerHTML = `
48+
<div>
49+
<img data-slot="cover" alt="" style="width: 100%; max-height: 16rem; object-fit: cover;
50+
border-radius: var(--pf-t--global--border--radius--medium); margin-bottom: 1rem;" />
51+
<div class="pf-v6-c-description-list pf-m-horizontal pf-m-compact">
52+
<div class="pf-v6-c-description-list__group" data-slot="date-group">
53+
<dt class="pf-v6-c-description-list__term"><span class="pf-v6-c-description-list__text">Date</span></dt>
54+
<dd class="pf-v6-c-description-list__description"><div class="pf-v6-c-description-list__text" data-slot="date"></div></dd>
55+
</div>
56+
<div class="pf-v6-c-description-list__group" data-slot="time-group">
57+
<dt class="pf-v6-c-description-list__term"><span class="pf-v6-c-description-list__text">Time</span></dt>
58+
<dd class="pf-v6-c-description-list__description"><div class="pf-v6-c-description-list__text" data-slot="time"></div></dd>
59+
</div>
60+
<div class="pf-v6-c-description-list__group" data-slot="venue-group">
61+
<dt class="pf-v6-c-description-list__term"><span class="pf-v6-c-description-list__text">Venue</span></dt>
62+
<dd class="pf-v6-c-description-list__description"><div class="pf-v6-c-description-list__text"><span data-slot="venue"></span><br data-slot="map-br" /><span data-slot="map-link"></span></div></dd>
63+
</div>
64+
<div class="pf-v6-c-description-list__group" data-slot="hosts-group">
65+
<dt class="pf-v6-c-description-list__term"><span class="pf-v6-c-description-list__text">Hosted by</span></dt>
66+
<dd class="pf-v6-c-description-list__description"><div class="pf-v6-c-description-list__text" data-slot="hosts"></div></dd>
67+
</div>
68+
<div class="pf-v6-c-description-list__group" data-slot="admission-group">
69+
<dt class="pf-v6-c-description-list__term"><span class="pf-v6-c-description-list__text">Admission</span></dt>
70+
<dd class="pf-v6-c-description-list__description"><div class="pf-v6-c-description-list__text" data-slot="admission"></div></dd>
71+
</div>
72+
</div>
73+
<hr class="pf-v6-u-mt-md pf-v6-u-mb-md" data-slot="desc-divider" />
74+
<div class="pf-v6-c-content" data-slot="description"></div>
75+
</div>`;
76+
77+
customElements.define(
78+
"event-detail-modal",
79+
class extends HTMLElement {
80+
connectedCallback() {
81+
this.append(modalTemplate.content.cloneNode(true));
82+
this._backdrop = this.querySelector(`.${backdropClass}`);
83+
this._$ = (s) => this.querySelector(`[data-slot="${s}"]`);
84+
85+
this._$("close").addEventListener("click", () => this.close());
86+
this._$("cancel").addEventListener("click", () => this.close());
87+
this._backdrop.addEventListener("click", (e) => {
88+
if (e.target === this._backdrop) this.close();
89+
});
90+
this._keyHandler = (e) => {
91+
if (e.key === "Escape") this.close();
92+
};
93+
}
94+
95+
async open(eventApiId, api) {
96+
this._backdrop.style.display = "";
97+
document.documentElement.style.overflow = "hidden";
98+
document.addEventListener("keydown", this._keyHandler);
99+
100+
this._$("title").innerHTML = '<div class="pf-v6-c-skeleton pf-m-text-lg pf-m-width-50"></div>';
101+
const content = this._$("content");
102+
content.innerHTML = `
103+
<div class="pf-v6-c-skeleton pf-v6-u-mb-md" style="--pf-v6-c-skeleton--Height: 10rem; border-radius: var(--pf-t--global--border--radius--medium);"></div>
104+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75 pf-v6-u-mb-sm"></div>
105+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50 pf-v6-u-mb-sm"></div>
106+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75 pf-v6-u-mb-sm"></div>
107+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50 pf-v6-u-mb-md"></div>
108+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-v6-u-mb-sm"></div>
109+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-75 pf-v6-u-mb-sm"></div>
110+
<div class="pf-v6-c-skeleton pf-m-text-sm pf-m-width-50"></div>`;
111+
this._$("rsvp").style.display = "none";
112+
113+
try {
114+
const data = await fetch(
115+
`${api}/events/detail?event_api_id=${encodeURIComponent(eventApiId)}`,
116+
).then((r) => r.json());
117+
118+
this._$("title").textContent = data.name;
119+
this._$("rsvp").href = data.link;
120+
this._$("rsvp").style.display = "";
121+
122+
const detail = detailTemplate.content.cloneNode(true);
123+
const $ = (s) => detail.querySelector(`[data-slot="${s}"]`);
124+
125+
const cover = $("cover");
126+
if (data.cover_url) {
127+
cover.src = proxyImageUrl(data.cover_url, api, "luma");
128+
cover.alt = data.name;
129+
} else {
130+
cover.remove();
131+
}
132+
133+
const start = new Date(data.start_at);
134+
const dateOpts = {
135+
weekday: "long",
136+
year: "numeric",
137+
month: "long",
138+
day: "numeric",
139+
timeZone: data.timezone || undefined,
140+
};
141+
$("date").textContent = start.toLocaleDateString("en-CA", dateOpts);
142+
143+
const timeOpts = {
144+
hour: "numeric",
145+
minute: "2-digit",
146+
timeZone: data.timezone || undefined,
147+
timeZoneName: data.timezone ? "short" : undefined,
148+
};
149+
let timeText = start.toLocaleTimeString("en-CA", timeOpts);
150+
if (data.end_at) {
151+
const end = new Date(data.end_at);
152+
timeText += " – " + end.toLocaleTimeString("en-CA", timeOpts);
153+
}
154+
$("time").textContent = timeText;
155+
156+
if (data.location) {
157+
const venueText = data.full_address
158+
? `${data.location}${data.full_address}`
159+
: data.location;
160+
$("venue").textContent = venueText;
161+
162+
if (data.map_url) {
163+
const mapLink = document.createElement("a");
164+
mapLink.href = data.map_url;
165+
mapLink.target = "_blank";
166+
mapLink.rel = "noopener";
167+
mapLink.className = "pf-v6-c-button pf-m-link pf-m-inline pf-m-small";
168+
mapLink.innerHTML =
169+
'View on map <span class="pf-v6-c-button__icon pf-m-end"><icon-external></icon-external></span>';
170+
$("map-link").append(mapLink);
171+
} else {
172+
$("map-br").remove();
173+
$("map-link").remove();
174+
}
175+
} else {
176+
$("venue-group").remove();
177+
}
178+
179+
if (data.hosts) {
180+
$("hosts").textContent = data.hosts;
181+
} else {
182+
$("hosts-group").remove();
183+
}
184+
185+
if (data.admission) {
186+
$("admission").textContent = data.admission;
187+
} else {
188+
$("admission-group").remove();
189+
}
190+
191+
if (data.description) {
192+
$("description").innerHTML = DOMPurify.sanitize(data.description);
193+
} else {
194+
$("desc-divider").remove();
195+
$("description").remove();
196+
}
197+
198+
content.replaceChildren(detail);
199+
} catch {
200+
content.innerHTML =
201+
"<p>Could not load event details. Please try again later.</p>";
202+
}
203+
}
204+
205+
close() {
206+
this._backdrop.style.display = "none";
207+
document.documentElement.style.overflow = "";
208+
document.removeEventListener("keydown", this._keyHandler);
209+
}
210+
},
211+
);

assets/js/luma-next-event.js

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,27 @@ template.innerHTML = `
2424
<h4 data-slot="name"></h4>
2525
<p><small data-slot="detail"></small></p>
2626
</div>
27-
<a data-slot="rsvp" target="_blank" rel="noopener"
28-
class="pf-v6-c-button pf-m-link pf-m-inline">
29-
<span class="pf-v6-c-button__text">RSVP on Luma</span>
30-
<span class="pf-v6-c-button__icon pf-m-end"><icon-external></icon-external></span>
31-
</a>
27+
<div class="pf-v6-l-flex pf-m-gap-sm pf-m-align-items-center">
28+
<button data-slot="details" type="button"
29+
class="pf-v6-c-button pf-m-control pf-m-small">
30+
<span class="pf-v6-c-button__text">View details</span>
31+
</button>
32+
<span class="pf-v6-u-text-color-subtle pf-v6-u-font-size-sm">or</span>
33+
<a data-slot="rsvp" target="_blank" rel="noopener"
34+
class="pf-v6-c-button pf-m-link pf-m-inline">
35+
<span class="pf-v6-c-button__text">RSVP on Luma</span>
36+
<span class="pf-v6-c-button__icon pf-m-end"><icon-external></icon-external></span>
37+
</a>
38+
</div>
3239
</div>
3340
</div>
3441
</div>
3542
</div>
3643
</div>`;
3744

38-
const renderEventCard = (evt) => {
39-
const { name, date, time, relative, lumaUrl, location, coverUrl } =
40-
formatEvent(evt);
45+
const renderEventCard = (evt, api) => {
46+
const { apiId, name, date, time, relative, lumaUrl, location, coverUrl } =
47+
formatEvent(evt, api);
4148
const clone = template.content.cloneNode(true);
4249
const $ = (s) => clone.querySelector(`[data-slot="${s}"]`);
4350
const cover = $("cover");
@@ -52,6 +59,9 @@ const renderEventCard = (evt) => {
5259
$("name").textContent = name;
5360
$("detail").textContent = location ? `${location} \u00b7 ${time}` : time;
5461
$("rsvp").href = lumaUrl;
62+
$("details").addEventListener("click", () => {
63+
document.querySelector("event-detail-modal").open(apiId, api);
64+
});
5565
return clone;
5666
};
5767

@@ -70,7 +80,7 @@ customElements.define(
7080
this.textContent = "No upcoming events right now. Check back soon!";
7181
return;
7282
}
73-
this.replaceChildren(renderEventCard(evt));
83+
this.replaceChildren(renderEventCard(evt, api));
7484
} catch {
7585
this.textContent = "Could not load the next event. ";
7686
const a = document.createElement("a");

assets/js/luma-upcoming-events.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@ rowTemplate.innerHTML = `
1717
<td class="pf-v6-c-table__td pf-v6-u-display-none pf-v6-u-display-table-cell-on-md" role="cell" style="vertical-align: middle;" data-slot="name-desktop"></td>
1818
<td class="pf-v6-c-table__td" role="cell" style="vertical-align: middle;" data-slot="location"></td>
1919
<td class="pf-v6-c-table__td" role="cell" style="vertical-align: middle;">
20-
<a data-slot="rsvp" target="_blank" rel="noopener" class="pf-v6-c-button pf-m-link pf-m-inline pf-m-small">
21-
RSVP<span class="pf-v6-c-button__icon pf-m-end"><icon-external></icon-external></span>
22-
</a>
20+
<div class="pf-v6-l-flex pf-m-gap-sm">
21+
<button data-slot="details" type="button" class="pf-v6-c-button pf-m-control pf-m-small">
22+
Details
23+
</button>
24+
<a data-slot="rsvp" target="_blank" rel="noopener" class="pf-v6-c-button pf-m-link pf-m-inline pf-m-small">
25+
RSVP<span class="pf-v6-c-button__icon pf-m-end"><icon-external></icon-external></span>
26+
</a>
27+
</div>
2328
</td>
2429
</tr>`;
2530

26-
const renderEventRow = (evt) => {
27-
const { name, date, time, relative, lumaUrl, location, coverUrl } =
28-
formatEvent(evt);
31+
const renderEventRow = (evt, api) => {
32+
const { apiId, name, date, time, relative, lumaUrl, location, coverUrl } =
33+
formatEvent(evt, api);
2934
const clone = rowTemplate.content.cloneNode(true);
3035
const $ = (s) => clone.querySelector(`[data-slot="${s}"]`);
3136

@@ -42,6 +47,9 @@ const renderEventRow = (evt) => {
4247
$("name-desktop").textContent = name;
4348
$("location").textContent = location;
4449
$("rsvp").href = lumaUrl;
50+
$("details").addEventListener("click", () => {
51+
document.querySelector("event-detail-modal").open(apiId, api);
52+
});
4553

4654
return clone;
4755
};
@@ -86,7 +94,7 @@ customElements.define(
8694
tbody.className = "pf-v6-c-table__tbody";
8795
tbody.setAttribute("role", "rowgroup");
8896
for (const evt of entries) {
89-
tbody.append(renderEventRow(evt));
97+
tbody.append(renderEventRow(evt, api));
9098
}
9199
table.append(tbody);
92100

assets/js/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "./theme.js";
22
import "./sidebar.js";
33
import "./icons.js";
4+
import "./event-detail-modal.js";
45
import "./luma-next-event.js";
56
import "./luma-upcoming-events.js";
67
import "./mastodon-feed.js";

layouts/baseof.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@
106106
</main>
107107
</div>
108108
{{ end }}
109+
110+
<event-detail-modal></event-detail-modal>
109111
</div>
110112

111113
{{ with resources.Get "js/main.js" }}

main.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,20 @@ func main() {
130130
handlers.EventsFeedHandler(w, r, apiBase, eventDetailBase, lumaBase, mapBase, siteURL)
131131
}))
132132

133+
mux.HandleFunc("/events/detail", cors(func(w http.ResponseWriter, r *http.Request) {
134+
defer func() {
135+
if err := recover(); err != nil {
136+
log.Println("Error occured in event detail API:", err)
137+
138+
http.Error(w, "Error occured in event detail API", http.StatusInternalServerError)
139+
140+
return
141+
}
142+
}()
143+
144+
handlers.EventDetailHandler(w, r, eventDetailBase, lumaBase, mapBase)
145+
}))
146+
133147
mux.HandleFunc("/mastodon", cors(func(w http.ResponseWriter, r *http.Request) {
134148
defer func() {
135149
if err := recover(); err != nil {

pkg/handlers/feed_entry.html.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
{{ if .Description -}}
2828
<hr/>
29-
{{ .Description }}
29+
{{ .Description | safeHTML }}
3030
{{ end -}}
3131

3232
<p><a href="{{ .Link }}">RSVP on Luma</a></p>

0 commit comments

Comments
 (0)