Skip to content

Commit 921786e

Browse files
committed
feat: update speaking events structure to include event type and format, enhance filtering and display logic
1 parent 8445a75 commit 921786e

3 files changed

Lines changed: 220 additions & 105 deletions

File tree

app/pages/speaking.vue

Lines changed: 121 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
interface 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')
2930
const 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
4950
const 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
6077
const 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+
77114
function 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>

content.config.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,9 @@ export const collections = {
145145
z.object({
146146
name: z.string().optional(),
147147
date: z.string().optional(),
148-
type: z.enum(['conference', 'meetup', 'online', 'podcast']).optional(),
149-
conference: z.string().optional(),
148+
event: z.string().optional(),
149+
type: z.enum(['conference', 'meetup', 'podcast', 'webinar']).optional(),
150+
format: z.enum(['talk', 'workshop', 'panel', 'keynote', 'lightning']).optional(),
150151
location: z.string().optional(),
151152
url: z.string().optional(),
152153
slides: z.string().optional(),

0 commit comments

Comments
 (0)