Skip to content

Commit 8445a75

Browse files
committed
(tmp) feat: add speaking events page and configuration for public speaking engagements
1 parent 7faf87a commit 8445a75

3 files changed

Lines changed: 346 additions & 0 deletions

File tree

app/pages/speaking.vue

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<script setup lang="ts">
2+
interface SpeakingEvent {
3+
name?: string
4+
date?: string
5+
type?: 'conference' | 'meetup' | 'online' | 'podcast'
6+
conference?: string
7+
location?: string
8+
url?: string
9+
slides?: string
10+
image?: string
11+
speakers?: string[]
12+
}
13+
14+
const { data: page } = await useAsyncData('speaking', () => queryCollection('speaking').first())
15+
16+
const title = page.value?.seo?.title || page.value?.title || 'Speaking'
17+
const description = page.value?.seo?.description || page.value?.description
18+
19+
useSeoMeta({
20+
title,
21+
ogTitle: title,
22+
description,
23+
ogDescription: description
24+
})
25+
26+
defineOgImageComponent('Saas')
27+
28+
// Sort events by date (most recent first) and filter out incomplete entries
29+
const events = computed(() => {
30+
if (!page.value?.events) return []
31+
return [...page.value.events]
32+
.filter((e: SpeakingEvent) => e.name && e.conference)
33+
.sort((a: SpeakingEvent, b: SpeakingEvent) => {
34+
if (!a.date) return 1
35+
if (!b.date) return -1
36+
return new Date(b.date).getTime() - new Date(a.date).getTime()
37+
})
38+
})
39+
40+
const upcomingEvents = computed(() =>
41+
events.value.filter((e: SpeakingEvent) => e.date && new Date(e.date) > new Date())
42+
)
43+
44+
const pastEvents = computed(() =>
45+
events.value.filter((e: SpeakingEvent) => !e.date || new Date(e.date) <= new Date())
46+
)
47+
48+
// Group past events by year for timeline
49+
const eventsByYear = computed(() => {
50+
const groups: Record<string, SpeakingEvent[]> = {}
51+
pastEvents.value.forEach((event: SpeakingEvent) => {
52+
const year = event.date ? new Date(event.date).getFullYear().toString() : 'Unknown'
53+
if (!groups[year]) groups[year] = []
54+
groups[year].push(event)
55+
})
56+
return Object.entries(groups).sort(([a], [b]) => Number(b) - Number(a))
57+
})
58+
59+
// Stats
60+
const stats = computed(() => ({
61+
totalTalks: events.value.length,
62+
conferences: new Set(events.value.map((e: SpeakingEvent) => e.conference)).size,
63+
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
65+
}))
66+
67+
// Icon based on event type
68+
function getEventIcon(type?: string) {
69+
switch (type) {
70+
case 'online': return 'i-lucide-video'
71+
case 'podcast': return 'i-lucide-mic'
72+
case 'meetup': return 'i-lucide-users'
73+
default: return 'i-lucide-presentation'
74+
}
75+
}
76+
77+
function formatDate(date?: string) {
78+
if (!date) return ''
79+
return new Date(date).toLocaleDateString('en', {
80+
year: 'numeric',
81+
month: 'short',
82+
day: 'numeric'
83+
})
84+
}
85+
</script>
86+
87+
<template>
88+
<UContainer>
89+
<UPageHeader
90+
:title="page?.title || 'Speaking'"
91+
:description="page?.description"
92+
class="py-[50px]"
93+
>
94+
<template #headline>
95+
<UBadge
96+
variant="subtle"
97+
icon="i-lucide-mic"
98+
label="Public Speaking"
99+
/>
100+
</template>
101+
</UPageHeader>
102+
103+
<UPageBody>
104+
<!-- Stats Overview -->
105+
<section class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12">
106+
<UPageCard variant="soft" class="text-center">
107+
<div class="text-3xl font-bold text-primary">
108+
{{ stats.totalTalks }}
109+
</div>
110+
<div class="text-muted text-sm">
111+
Talks Given
112+
</div>
113+
</UPageCard>
114+
<UPageCard variant="soft" class="text-center">
115+
<div class="text-3xl font-bold text-primary">
116+
{{ stats.conferences }}
117+
</div>
118+
<div class="text-muted text-sm">
119+
Conferences
120+
</div>
121+
</UPageCard>
122+
<UPageCard variant="soft" class="text-center">
123+
<div class="text-3xl font-bold text-primary">
124+
{{ stats.years }}
125+
</div>
126+
<div class="text-muted text-sm">
127+
Years Speaking
128+
</div>
129+
</UPageCard>
130+
<UPageCard variant="soft" class="text-center">
131+
<div class="text-3xl font-bold text-primary">
132+
{{ stats.withVideo }}
133+
</div>
134+
<div class="text-muted text-sm">
135+
Recorded Talks
136+
</div>
137+
</UPageCard>
138+
</section>
139+
140+
<!-- Upcoming Events -->
141+
<section v-if="upcomingEvents.length" class="mb-16">
142+
<h2 class="text-2xl font-bold mb-6 flex items-center gap-2">
143+
<UIcon name="i-lucide-calendar-clock" class="text-primary" />
144+
Upcoming Events
145+
</h2>
146+
<UPageGrid>
147+
<UPageCard
148+
v-for="event in upcomingEvents"
149+
:key="`${event.conference}-${event.date}`"
150+
:title="event.name"
151+
:description="event.conference"
152+
:icon="getEventIcon(event.type)"
153+
:to="event.url"
154+
:target="event.url ? '_blank' : undefined"
155+
highlight
156+
highlight-color="primary"
157+
variant="outline"
158+
>
159+
<template #footer>
160+
<div class="flex items-center justify-between gap-2 flex-wrap">
161+
<UBadge
162+
:label="formatDate(event.date)"
163+
variant="subtle"
164+
icon="i-lucide-calendar"
165+
/>
166+
<UBadge
167+
v-if="event.location"
168+
:label="event.location"
169+
color="neutral"
170+
variant="soft"
171+
icon="i-lucide-map-pin"
172+
/>
173+
</div>
174+
</template>
175+
</UPageCard>
176+
</UPageGrid>
177+
</section>
178+
179+
<!-- Past Events by Year -->
180+
<section>
181+
<h2 class="text-2xl font-bold mb-8 flex items-center gap-2">
182+
<UIcon name="i-lucide-history" class="text-primary" />
183+
Past Talks
184+
</h2>
185+
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>
190+
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"
197+
>
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>
205+
<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) }}
217+
</p>
218+
</div>
219+
</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>
247+
</div>
248+
</div>
249+
</section>
250+
251+
<!-- Contact CTA -->
252+
<section class="mt-16">
253+
<UPageCard variant="subtle" class="text-center max-w-2xl mx-auto">
254+
<div class="flex flex-col items-center gap-4 py-4">
255+
<div class="p-3 rounded-full bg-primary/10">
256+
<UIcon name="i-lucide-message-circle" class="size-8 text-primary" />
257+
</div>
258+
<h3 class="text-xl font-semibold text-highlighted">
259+
Want me to speak at your event?
260+
</h3>
261+
<p class="text-muted max-w-md">
262+
I'd love to share knowledge about Cloud, Infrastructure as Code, Pulumi, and .NET development at your conference or meetup.
263+
</p>
264+
<UButton
265+
to="/contact"
266+
label="Get in Touch"
267+
icon="i-lucide-send"
268+
size="lg"
269+
class="mt-2"
270+
/>
271+
</div>
272+
</UPageCard>
273+
</section>
274+
</UPageBody>
275+
</UContainer>
276+
</template>

content.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,26 @@ export const collections = {
136136
image: createImageSchema().optional()
137137
})
138138
}),
139+
speaking: defineCollection({
140+
source: '2.speaking.yml',
141+
type: 'page',
142+
schema: z.object({
143+
align: z.string().optional(),
144+
events: z.array(
145+
z.object({
146+
name: z.string().optional(),
147+
date: z.string().optional(),
148+
type: z.enum(['conference', 'meetup', 'online', 'podcast']).optional(),
149+
conference: z.string().optional(),
150+
location: z.string().optional(),
151+
url: z.string().optional(),
152+
slides: z.string().optional(),
153+
image: z.string().optional(),
154+
speakers: z.array(z.string()).optional()
155+
})
156+
).optional()
157+
})
158+
}),
139159
content: defineCollection({
140160
source: '*.md',
141161
type: 'page',

content/2.speaking.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
navigation.icon: i-ph-microphone-stage
2+
title: Speaking
3+
description: Talks, conferences and presentations about Cloud, Infrastructure as Code, and .NET development.
4+
seo:
5+
title: Speaking - Alexandre Nédélec
6+
description: Discover my talks and presentations at conferences about Cloud, DevOps, Pulumi, and .NET development.
7+
events:
8+
- name: Pulumi vs. Terraform
9+
date: 2022-03-22
10+
type: online
11+
conference: Clash
12+
url: https://www.youtube.com/live/7raXBE5XH7Y?si=7hYJu702DxMSZBjo
13+
- name: Infrastructure as Code or Infrastructure as Software
14+
date: 2022-06-30
15+
type: conference
16+
conference: Cloud Ouest 2022
17+
- name: How Pulumi is revolutionizing infrastructure deployment in Azure
18+
date: 2023-05-13
19+
type: conference
20+
conference: Global Azure France 2023
21+
- name: IaC to the future with Pulumi
22+
date: 2023-06-20
23+
type: conference
24+
conference: Cloud Est 2023
25+
- name: Infrastructure as Code or Infrastructure as Software
26+
date: 2023-06-29
27+
type: conference
28+
conference: BreizhCamp 2023
29+
url: https://youtu.be/vhJfNetCp9Y?si=fkbiyM00DSwNGj6G
30+
- name: Oops I forgot to make my slides
31+
date: 2023-11-15
32+
type: conference
33+
conference: BDX I/O 2023
34+
location: Bordeaux
35+
url: https://youtu.be/XZAIb5CLqok?si=wrQ2Ive-o9DjlNZV
36+
- name: Industrialize the configuration of your GitHub repositories using IaC
37+
date: 2024-09-18
38+
type: conference
39+
conference: PulumiUP 2024
40+
location: Virtual
41+
- name: Infrastructure as Code with Pulumi
42+
date: 2024-10-10
43+
type: conference
44+
conference: Cloud Nord 2024
45+
location: Lille
46+
- name: Infrastructure as Code with Pulumi
47+
date: 2024-11-15
48+
type: conference
49+
conference: BDX I/O 2024
50+
location: Bordeaux

0 commit comments

Comments
 (0)