22interface SpeakingEvent {
33 name? : string
44 date? : string
5- type? : ' conference' | ' meetup' | ' online' | ' podcast'
6- conference? : string
5+ event? : string
6+ type? : ' conference' | ' meetup' | ' podcast' | ' webinar'
7+ format? : ' talk' | ' workshop' | ' panel' | ' keynote' | ' lightning'
78 location? : string
89 url? : string
910 slides? : string
@@ -29,7 +30,7 @@ defineOgImageComponent('Saas')
2930const events = computed (() => {
3031 if (! page .value ?.events ) return []
3132 return [... page .value .events ]
32- .filter ((e : SpeakingEvent ) => e .name && e .conference )
33+ .filter ((e : SpeakingEvent ) => e .name && e .event )
3334 .sort ((a : SpeakingEvent , b : SpeakingEvent ) => {
3435 if (! a .date ) return 1
3536 if (! b .date ) return - 1
@@ -45,35 +46,71 @@ const pastEvents = computed(() =>
4546 events .value .filter ((e : SpeakingEvent ) => ! e .date || new Date (e .date ) <= new Date ())
4647)
4748
48- // Group past events by year for timeline
49+ // Group past events by year with timeline items
4950const eventsByYear = computed (() => {
5051 const groups: Record <string , SpeakingEvent []> = {}
5152 pastEvents .value .forEach ((event : SpeakingEvent ) => {
5253 const year = event .date ? new Date (event .date ).getFullYear ().toString () : ' Unknown'
5354 if (! groups [year ]) groups [year ] = []
5455 groups [year ].push (event )
5556 })
56- return Object .entries (groups ).sort (([a ], [b ]) => Number (b ) - Number (a ))
57+ return Object .entries (groups )
58+ .sort (([a ], [b ]) => Number (b ) - Number (a ))
59+ .map (([year , yearEvents ]) => ({
60+ year ,
61+ timelineItems: yearEvents .map ((event , index ) => ({
62+ date: formatDate (event .date ),
63+ title: event .event ,
64+ icon: getTypeIcon (event .type ),
65+ value: index ,
66+ talkName: event .name ,
67+ format: event .format ,
68+ location: event .location ,
69+ isOnline: event .location === ' Online' ,
70+ url: event .url ,
71+ slides: event .slides
72+ }))
73+ }))
5774})
5875
5976// Stats
6077const stats = computed (() => ({
6178 totalTalks: events .value .length ,
62- conferences: new Set ( events .value .map ((e : SpeakingEvent ) => e .conference )). size ,
79+ conferences: events .value .filter ((e : SpeakingEvent ) => e .type === ' conference' ). length ,
6380 years: new Set (events .value .map ((e : SpeakingEvent ) => e .date ? new Date (e .date ).getFullYear () : null ).filter (Boolean )).size ,
64- withVideo : events .value .filter ((e : SpeakingEvent ) => e .url ).length
81+ podcasts : events .value .filter ((e : SpeakingEvent ) => e .type === ' podcast ' ).length
6582}))
6683
6784// Icon based on event type
68- function getEventIcon (type ? : string ) {
85+ function getTypeIcon (type ? : string ) {
6986 switch (type ) {
70- case ' online' : return ' i-lucide-video'
7187 case ' podcast' : return ' i-lucide-mic'
88+ case ' webinar' : return ' i-lucide-video'
7289 case ' meetup' : return ' i-lucide-users'
7390 default : return ' i-lucide-presentation'
7491 }
7592}
7693
94+ // Format badge color
95+ function getFormatColor(format ? : string ) {
96+ switch (format ) {
97+ case ' workshop' : return ' warning'
98+ case ' panel' : return ' info'
99+ case ' keynote' : return ' error'
100+ case ' lightning' : return ' neutral'
101+ case ' interview' : return ' secondary'
102+ default : return ' success'
103+ }
104+ }
105+
106+ // Format display label
107+ function getFormatLabel(format ? : string ) {
108+ switch (format ) {
109+ case ' workshop' : return ' hands-on lab'
110+ default : return format
111+ }
112+ }
113+
77114function formatDate(date ? : string ) {
78115 if (! date ) return ' '
79116 return new Date (date ).toLocaleDateString (' en' , {
@@ -108,7 +145,7 @@ function formatDate(date?: string) {
108145 {{ stats.totalTalks }}
109146 </div >
110147 <div class =" text-muted text-sm" >
111- Talks Given
148+ Speaking Events
112149 </div >
113150 </UPageCard >
114151 <UPageCard variant =" soft" class =" text-center" >
@@ -129,10 +166,10 @@ function formatDate(date?: string) {
129166 </UPageCard >
130167 <UPageCard variant =" soft" class =" text-center" >
131168 <div class =" text-3xl font-bold text-primary" >
132- {{ stats.withVideo }}
169+ {{ stats.podcasts }}
133170 </div >
134171 <div class =" text-muted text-sm" >
135- Recorded Talks
172+ Podcasts
136173 </div >
137174 </UPageCard >
138175 </section >
@@ -146,10 +183,10 @@ function formatDate(date?: string) {
146183 <UPageGrid >
147184 <UPageCard
148185 v-for =" event in upcomingEvents"
149- :key =" `${event.conference }-${event.date}`"
186+ :key =" `${event.event }-${event.date}`"
150187 :title =" event.name"
151- :description =" event.conference "
152- :icon =" getEventIcon (event.type)"
188+ :description =" event.event "
189+ :icon =" getTypeIcon (event.type)"
153190 :to =" event.url"
154191 :target =" event.url ? '_blank' : undefined"
155192 highlight
@@ -176,74 +213,86 @@ function formatDate(date?: string) {
176213 </UPageGrid >
177214 </section >
178215
179- <!-- Past Events by Year -->
216+ <!-- Past Events as Timeline grouped by Year -->
180217 <section >
181- <h2 class =" text-2xl font-bold mb-8 flex items-center gap-2" >
218+ <h2 class =" text-2xl font-bold mb-8 flex items-center gap-2 justify-center " >
182219 <UIcon name =" i-lucide-history" class =" text-primary" />
183220 Past Talks
184221 </h2 >
185222
186- <div v-for =" [year, yearEvents] in eventsByYear" :key =" year" class =" mb-12" >
187- <h3 class =" text-xl font-semibold mb-4 text-muted border-b border-default pb-2" >
188- {{ year }}
189- </h3 >
223+ <div class =" max-w-2xl mx-auto space-y-12" >
224+ <div v-for =" { year, timelineItems } in eventsByYear" :key =" year" >
225+ <!-- Year Header -->
226+ <div class =" flex items-center gap-4 mb-6" >
227+ <div class =" h-px flex-1 bg-default" />
228+ <span class =" text-xl font-bold text-primary px-4 py-1 rounded-full bg-primary/10" >
229+ {{ year }}
230+ </span >
231+ <div class =" h-px flex-1 bg-default" />
232+ </div >
190233
191- < div class = " space-y-4 " >
192- <UPageCard
193- v-for = " event in yearEvents "
194- :key = " `${event.conference}-${event.date}-${event.name}` "
195- variant = " subtle "
196- class = " group "
234+ <!-- Timeline for this year -- >
235+ <UTimeline
236+ :items = " timelineItems "
237+ :default-value = " timelineItems.length - 1 "
238+ color = " primary "
239+ size = " md "
197240 >
198- <div class = " flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 " >
199- <!-- Event Info -->
200- < div class = " flex-1 min-w-0 " >
201- < div class =" flex items-start gap-3 " >
202- < div class = " shrink-0 p-2 rounded-lg bg-primary/10 text-primary " >
203- < UIcon :name = " getEventIcon(event.type) " class = " size-5 " / >
204- </ div >
241+ <template # description = " { item } " >
242+ <UPageCard
243+ variant = " subtle "
244+ class =" mt-1 "
245+ :ui = " { container: 'p-3 sm:p-4' } "
246+ >
247+ < div class = " flex items-center justify-between gap-4 " >
205248 <div class =" flex-1 min-w-0" >
206- <h4 class =" font-semibold text-highlighted truncate" >
207- {{ event.name }}
208- </h4 >
209- <p class =" text-muted text-sm" >
210- {{ event.conference }}
211- <span v-if =" event.location" class =" text-dimmed" >
212- · {{ event.location }}
213- </span >
214- </p >
215- <p v-if =" event.date" class =" text-dimmed text-xs mt-1" >
216- {{ formatDate(event.date) }}
249+ <div class =" flex items-start gap-2" >
250+ <h4 class =" font-semibold text-highlighted" >
251+ {{ item.talkName }}
252+ </h4 >
253+ <UBadge
254+ v-if =" item.format"
255+ :label =" getFormatLabel(item.format)"
256+ :color =" getFormatColor(item.format)"
257+ variant =" subtle"
258+ size =" sm"
259+ class =" capitalize shrink-0"
260+ />
261+ </div >
262+ <p v-if =" item.location" class =" text-muted text-sm mt-0.5 flex items-center gap-1" >
263+ <UIcon
264+ :name =" item.isOnline ? 'i-lucide-globe' : 'i-lucide-map-pin'"
265+ class =" size-3.5"
266+ />
267+ {{ item.location }}
217268 </p >
218269 </div >
270+ <div v-if =" item.url || item.slides" class =" flex items-center gap-2 shrink-0" >
271+ <UButton
272+ v-if =" item.url"
273+ :to =" item.url"
274+ target =" _blank"
275+ :icon =" item.icon === 'i-lucide-mic' ? 'i-lucide-headphones' : 'i-lucide-play-circle'"
276+ :label =" item.icon === 'i-lucide-mic' ? 'Listen' : 'Watch'"
277+ size =" xs"
278+ variant =" soft"
279+ color =" primary"
280+ />
281+ <UButton
282+ v-if =" item.slides"
283+ :to =" item.slides"
284+ target =" _blank"
285+ icon =" i-lucide-presentation"
286+ label =" Slides"
287+ size =" xs"
288+ variant =" outline"
289+ color =" neutral"
290+ />
291+ </div >
219292 </div >
220- </div >
221-
222- <!-- Action Buttons -->
223- <div class =" flex items-center gap-2 shrink-0" >
224- <UButton
225- v-if =" event.url"
226- :to =" event.url"
227- target =" _blank"
228- icon =" i-lucide-play-circle"
229- label =" Watch"
230- size =" sm"
231- variant =" soft"
232- color =" primary"
233- />
234- <UButton
235- v-if =" event.slides"
236- :to =" event.slides"
237- target =" _blank"
238- icon =" i-lucide-presentation"
239- label =" Slides"
240- size =" sm"
241- variant =" outline"
242- color =" neutral"
243- />
244- </div >
245- </div >
246- </UPageCard >
293+ </UPageCard >
294+ </template >
295+ </UTimeline >
247296 </div >
248297 </div >
249298 </section >
0 commit comments