initial commit
Some checks failed
ci / ci (22, ubuntu-latest) (push) Failing after 42s

This commit is contained in:
2025-12-02 23:50:28 -05:00
commit 0def745b98
50 changed files with 14257 additions and 0 deletions

62
app/pages/about.vue Normal file
View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
const { data: page } = await useAsyncData('about', () => {
return queryCollection('about').first()
})
if (!page.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page not found',
fatal: true
})
}
const { global } = useAppConfig()
useSeoMeta({
title: page.value?.seo?.title || page.value?.title,
ogTitle: page.value?.seo?.title || page.value?.title,
description: page.value?.seo?.description || page.value?.description,
ogDescription: page.value?.seo?.description || page.value?.description
})
</script>
<template>
<UPage v-if="page">
<UPageHero
:title="page.title"
:description="page.description"
orientation="horizontal"
:ui="{
container: 'lg:flex sm:flex-row items-center',
title: '!mx-0 text-left',
description: '!mx-0 text-left',
links: 'justify-start'
}"
>
<UColorModeAvatar
class="sm:rotate-4 size-36 rounded-lg ring ring-default ring-offset-3 ring-offset-(--ui-bg)"
:light="global.picture?.light!"
:dark="global.picture?.dark!"
:alt="global.picture?.alt!"
/>
</UPageHero>
<UPageSection
:ui="{
container: '!pt-0'
}"
>
<MDC
:value="page.content"
unwrap="p"
/>
<div class="flex flex-row justify-center items-center py-10 space-x-[-2rem]">
<PolaroidItem
v-for="(image, index) in page.images"
:key="index"
:image="image"
:index
/>
</div>
</UPageSection>
</UPage>
</template>

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import type { ContentNavigationItem } from '@nuxt/content'
import { mapContentNavigation } from '@nuxt/ui/utils/content'
import { findPageBreadcrumb } from '@nuxt/content/utils'
const route = useRoute()
const { data: page } = await useAsyncData(route.path, () =>
queryCollection('blog').path(route.path).first()
)
if (!page.value) throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
const { data: surround } = await useAsyncData(`${route.path}-surround`, () =>
queryCollectionItemSurroundings('blog', route.path, {
fields: ['description']
})
)
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation', ref([]))
const blogNavigation = computed(() => navigation.value.find(item => item.path === '/blog')?.children || [])
const breadcrumb = computed(() => mapContentNavigation(findPageBreadcrumb(blogNavigation?.value, page.value?.path)).map(({ icon, ...link }) => link))
if (page.value.image) {
defineOgImage({ url: page.value.image })
} else {
defineOgImageComponent('Blog', {
headline: breadcrumb.value.map(item => item.label).join(' > ')
}, {
fonts: ['Geist:400', 'Geist:600']
})
}
const title = page.value?.seo?.title || page.value?.title
const description = page.value?.seo?.description || page.value?.description
useSeoMeta({
title,
description,
ogDescription: description,
ogTitle: title
})
const articleLink = computed(() => `${window?.location}`)
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
})
}
</script>
<template>
<UMain class="mt-20 px-2">
<UContainer class="relative min-h-screen">
<UPage v-if="page">
<ULink
to="/blog"
class="text-sm flex items-center gap-1"
>
<UIcon name="lucide:chevron-left" />
Blog
</ULink>
<div class="flex flex-col gap-3 mt-8">
<div class="flex text-xs text-muted items-center justify-center gap-2">
<span v-if="page.date">
{{ formatDate(page.date) }}
</span>
<span v-if="page.date && page.minRead">
-
</span>
<span v-if="page.minRead">
{{ page.minRead }} MIN READ
</span>
</div>
<NuxtImg
:src="page.image"
:alt="page.title"
class="rounded-lg w-full h-[300px] object-cover object-center"
/>
<h1 class="text-4xl text-center font-medium max-w-3xl mx-auto mt-4">
{{ page.title }}
</h1>
<p class="text-muted text-center max-w-2xl mx-auto">
{{ page.description }}
</p>
<div class="flex items-center justify-center gap-2 mt-2">
<UUser
orientation="vertical"
color="neutral"
variant="outline"
class="justify-center items-center text-center"
v-bind="page.author"
/>
</div>
</div>
<UPageBody class="max-w-3xl mx-auto">
<ContentRenderer
v-if="page.body"
:value="page"
/>
<div class="flex items-center justify-end gap-2 text-sm text-muted">
<UButton
size="sm"
variant="link"
color="neutral"
label="Copy link"
@click="copyToClipboard(articleLink, 'Article link copied to clipboard')"
/>
</div>
<UContentSurround :surround />
</UPageBody>
</UPage>
</UContainer>
</UMain>
</template>

76
app/pages/blog/index.vue Normal file
View File

@@ -0,0 +1,76 @@
<script setup lang="ts">
const { data: page } = await useAsyncData('blog-page', () => {
return queryCollection('pages').path('/blog').first()
})
if (!page.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page not found',
fatal: true
})
}
const { data: posts } = await useAsyncData('blogs', () =>
queryCollection('blog').order('date', 'DESC').all()
)
if (!posts.value) {
throw createError({
statusCode: 404,
statusMessage: 'blogs posts not found',
fatal: true
})
}
useSeoMeta({
title: page.value?.seo?.title || page.value?.title,
ogTitle: page.value?.seo?.title || page.value?.title,
description: page.value?.seo?.description || page.value?.description,
ogDescription: page.value?.seo?.description || page.value?.description
})
</script>
<template>
<UPage v-if="page">
<UPageHero
:title="page.title"
:description="page.description"
:links="page.links"
:ui="{
title: '!mx-0 text-left',
description: '!mx-0 text-left',
links: 'justify-start'
}"
/>
<UPageSection
:ui="{
container: '!pt-0'
}"
>
<UBlogPosts orientation="vertical">
<Motion
v-for="(post, index) in posts"
:key="index"
:initial="{ opacity: 0, transform: 'translateY(10px)' }"
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
:transition="{ delay: 0.2 * index }"
:in-view-options="{ once: true }"
>
<UBlogPost
variant="naked"
orientation="horizontal"
:to="post.path"
v-bind="post"
:ui="{
root: 'md:grid md:grid-cols-2 group overflow-visible transition-all duration-300',
image:
'group-hover/blog-post:scale-105 rounded-lg shadow-lg border-4 border-muted ring-2 ring-default',
header:
index % 2 === 0
? 'sm:-rotate-1 overflow-visible'
: 'sm:rotate-1 overflow-visible'
}"
/>
</Motion>
</UBlogPosts>
</UPageSection>
</UPage>
</template>

37
app/pages/index.vue Normal file
View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
const { data: page } = await useAsyncData('index', () => {
return queryCollection('index').first()
})
if (!page.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page not found',
fatal: true
})
}
useSeoMeta({
title: page.value?.seo.title || page.value?.title,
ogTitle: page.value?.seo.title || page.value?.title,
description: page.value?.seo.description || page.value?.description,
ogDescription: page.value?.seo.description || page.value?.description
})
</script>
<template>
<UPage v-if="page">
<LandingHero :page />
<UPageSection
:ui="{
container: '!pt-0 lg:grid lg:grid-cols-2 lg:gap-8'
}"
>
<LandingAbout :page />
<LandingWorkExperience :page />
<LandingEducation :page />
</UPageSection>
<!-- <LandingBlog :page /> -->
<!-- <LandingTestimonials :page /> -->
<!-- <LandingFAQ :page /> -->
</UPage>
</template>

107
app/pages/projects.vue Normal file
View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
const { data: page } = await useAsyncData('projects-page', () => {
return queryCollection('pages').path('/projects').first()
})
if (!page.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page not found',
fatal: true
})
}
const { data: projects } = await useAsyncData('projects', () => {
return queryCollection('projects').all()
})
const { global } = useAppConfig()
useSeoMeta({
title: page.value?.seo?.title || page.value?.title,
ogTitle: page.value?.seo?.title || page.value?.title,
description: page.value?.seo?.description || page.value?.description,
ogDescription: page.value?.seo?.description || page.value?.description
})
</script>
<template>
<UPage v-if="page">
<UPageHero
:title="page.title"
:description="page.description"
:links="page.links"
:ui="{
title: '!mx-0 text-left',
description: '!mx-0 text-left',
links: 'justify-start'
}"
>
<template #links>
<div
v-if="page.links"
class="flex items-center gap-2"
>
<UButton
:label="page.links[0]?.label"
:to="global.meetingLink"
v-bind="page.links[0]"
/>
<UButton
:to="`mailto:${global.email}`"
v-bind="page.links[1]"
/>
</div>
</template>
</UPageHero>
<UPageSection
:ui="{
container: '!pt-0'
}"
>
<Motion
v-for="(project, index) in projects"
:key="project.title"
:initial="{ opacity: 0, transform: 'translateY(10px)' }"
:while-in-view="{ opacity: 1, transform: 'translateY(0)' }"
:transition="{ delay: 0.2 * index }"
:in-view-options="{ once: true }"
>
<UPageCard
:title="project.title"
:description="project.description"
:to="project.url"
orientation="horizontal"
variant="naked"
:reverse="index % 2 === 1"
class="group"
:ui="{
wrapper: 'max-sm:order-last'
}"
>
<template #leading>
<span class="text-sm text-muted">
{{ new Date(project.date).getFullYear() }}
</span>
</template>
<template #footer>
<ULink
:to="project.url"
class="text-sm text-primary flex items-center"
>
View Project
<UIcon
name="i-lucide-arrow-right"
class="size-4 text-primary transition-all opacity-0 group-hover:translate-x-1 group-hover:opacity-100"
/>
</ULink>
</template>
<img
:src="project.image"
:alt="project.title"
class="object-cover w-full h-48 rounded-lg"
>
</UPageCard>
</Motion>
</UPageSection>
</UPage>
</template>

128
app/pages/speaking.vue Normal file
View File

@@ -0,0 +1,128 @@
<script setup lang="ts">
type Event = {
title: string
date: string
location: string
url?: string
category: 'Conference' | 'Live talk' | 'Podcast'
}
const { data: page } = await useAsyncData('speaking', () => {
return queryCollection('speaking').first()
})
if (!page.value) {
throw createError({
statusCode: 404,
statusMessage: 'Page not found',
fatal: true
})
}
useSeoMeta({
title: page.value?.seo?.title || page.value?.title,
ogTitle: page.value?.seo?.title || page.value?.title,
description: page.value?.seo?.description || page.value?.description,
ogDescription: page.value?.seo?.description || page.value?.description
})
const { global } = useAppConfig()
const groupedEvents = computed((): Record<Event['category'], Event[]> => {
const events = page.value?.events || []
const grouped: Record<Event['category'], Event[]> = {
'Conference': [],
'Live talk': [],
'Podcast': []
}
for (const event of events) {
if (grouped[event.category]) grouped[event.category].push(event)
}
return grouped
})
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'long' })
}
</script>
<template>
<UPage v-if="page">
<UPageHero
:title="page.title"
:description="page.description"
:ui="{
title: '!mx-0 text-left',
description: '!mx-0 text-left',
links: 'justify-start'
}"
>
<template #links>
<UButton
v-if="page.links"
:to="`mailto:${global.email}`"
v-bind="page.links[0]"
/>
</template>
</UPageHero>
<UPageSection
:ui="{
container: '!pt-0'
}"
>
<div
v-for="(eventsInCategory, category) in groupedEvents"
:key="category"
class="grid grid-cols-1 lg:grid-cols-3 lg:gap-8 mb-16 last:mb-0"
>
<div class="lg:col-span-1 mb-4 lg:mb-0">
<h2
class="lg:sticky lg:top-16 text-xl font-semibold tracking-tight text-highlighted"
>
{{ category.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase()) }}s
</h2>
</div>
<div class="lg:col-span-2 space-y-8">
<div
v-for="(event, index) in eventsInCategory"
:key="`${category}-${index}`"
class="group relative pl-6 border-l border-default"
>
<NuxtLink
v-if="event.url"
:to="event.url"
class="absolute inset-0"
/>
<div class="mb-1 text-sm font-medium text-muted">
<span>{{ event.location }}</span>
<span
v-if="event.location && event.date"
class="mx-1"
>·</span>
<span v-if="event.date">{{ formatDate(event.date) }}</span>
</div>
<h3 class="text-lg font-semibold text-highlighted">
{{ event.title }}
</h3>
<UButton
v-if="event.url"
target="_blank"
:label="event.category === 'Podcast' ? 'Listen' : 'Watch'"
variant="link"
class="p-0 pt-2 gap-0"
>
<template #trailing>
<UIcon
name="i-lucide-arrow-right"
class="size-4 transition-all opacity-0 group-hover:translate-x-1 group-hover:opacity-100"
/>
</template>
</UButton>
</div>
</div>
</div>
</UPageSection>
</UPage>
</template>