|
92 | 92 |
|
93 | 93 | const eventTemplate = document.getElementById("next-event-template"); |
94 | 94 |
|
95 | | - const renderEventCard = (evt) => { |
| 95 | + const formatEvent = (evt) => { |
96 | 96 | 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 | + }; |
113 | 126 |
|
| 127 | + const renderEventCard = (evt) => { |
| 128 | + const { name, date, time, relative, lumaUrl, location, coverUrl } = |
| 129 | + formatEvent(evt); |
114 | 130 | const clone = eventTemplate.content.cloneNode(true); |
115 | 131 | const $ = (s) => clone.querySelector(`[data-slot="${s}"]`); |
116 | 132 | 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; |
120 | 136 | } else { |
121 | 137 | cover.remove(); |
122 | 138 | } |
123 | | - $("date").textContent = start.toLocaleDateString("en-US", { |
124 | | - weekday: "short", |
125 | | - month: "short", |
126 | | - day: "numeric", |
127 | | - }); |
| 139 | + $("date").textContent = date; |
128 | 140 | $("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; |
137 | 144 | return clone; |
138 | 145 | }; |
139 | 146 |
|
| 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 | + |
140 | 235 | customElements.define( |
141 | 236 | "luma-next-event", |
142 | 237 | class extends HTMLElement { |
143 | | - connectedCallback() { |
| 238 | + async connectedCallback() { |
144 | 239 | const cal = this.getAttribute("calendar"); |
145 | 240 | 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 | + } |
166 | 259 | } |
167 | 260 | }, |
168 | 261 | ); |
169 | 262 |
|
170 | 263 | customElements.define( |
171 | 264 | "luma-upcoming-events", |
172 | 265 | class extends HTMLElement { |
173 | | - connectedCallback() { |
| 266 | + async connectedCallback() { |
174 | 267 | const cal = this.getAttribute("calendar"); |
175 | 268 | 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 | + ); |
177 | 321 | .then((r) => r.json()) |
178 | 322 | .then(({ entries }) => { |
179 | 323 | if (!entries || entries.length === 0) { |
|
0 commit comments