@@ -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+
5776customElements . 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 } ,
0 commit comments