Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions app/components/OgImage/OgImageProject.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<script setup lang="ts">
/**
* OG image component for project pages.
* Rendered by Satori (not a browser) - must use inline styles, <img>, and hardcoded colors.
*/
defineProps<{
title?: string
description?: string
repoStars?: number
tags?: string[]
}>()
</script>

<template>
<div
:style="{
width: '1200px',
height: '630px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
backgroundColor: '#0a0a0b',
fontFamily: 'Inter, system-ui, sans-serif',
padding: '60px 80px',
position: 'relative',
overflow: 'hidden',
}"
>
<OgImageGlow />
<OgImageAvatar />

<!-- Main content -->
<div
:style="{
display: 'flex',
flexDirection: 'column',
flexWrap: 'nowrap',
maxWidth: '960px',
}"
>
<OgImageCategoryLabel text="Project" />

<!-- Project name with stars -->
<div
class="flex-row"
:style="{
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'center',
marginBottom: '20px',
}"
>
<span
:style="{
fontSize: '72px',
fontWeight: 800,
color: '#fafafa',
lineHeight: 1.05,
letterSpacing: '-0.03em',
}"
>
{{ title || 'Project' }}
</span>
<span
v-if="repoStars"
class="flex-row"
:style="{
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'center',
marginLeft: '20px',
}"
>
<!-- Phosphor star icon (regular weight) -->
<svg
height="28"
:style="{ width: '28px', height: '28px', marginRight: '6px' }"
viewBox="0 0 256 256"
width="28"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M239.18 97.26A16.38 16.38 0 0 0 224.92 86l-59-4.76l-22.78-55.09a16.36 16.36 0 0 0-30.27 0L90.11 81.23L31.08 86a16.46 16.46 0 0 0-9.37 28.86l45 38.83L53 211.75a16.38 16.38 0 0 0 24.5 17.82L128 198.49l50.53 31.08A16.4 16.4 0 0 0 203 211.75l-13.76-58.07l45-38.83a16.38 16.38 0 0 0 4.94-17.59m-15.34 5.47l-48.7 42a8 8 0 0 0-2.56 7.91l14.88 62.8a.37.37 0 0 1-.17.48c-.18.14-.23.11-.38 0l-54.72-33.65a8 8 0 0 0-8.38 0l-54.72 33.67c-.15.09-.2.12-.38 0a.37.37 0 0 1-.17-.48l14.88-62.8a8 8 0 0 0-2.56-7.91l-48.7-42c-.12-.1-.18-.16-.13-.31s.18-.22.36-.23l63.65-5.13a8 8 0 0 0 6.71-4.85l24.62-59.6c.08-.17.11-.25.3-.25s.22.08.29.25l24.63 59.6a8 8 0 0 0 6.71 4.85l63.65 5.13c.18 0 .31.08.36.23s0 .21-.13.31"
fill="#71717a"
/>
</svg>
<span
:style="{
fontSize: '28px',
color: '#71717a',
fontFamily: 'monospace',
}"
>
{{ repoStars }}
</span>
</span>
</div>

<OgImageDescription v-if="description" :max-length="120" :text="description" />

<!-- Tags -->
<div
v-if="tags?.length"
class="flex-row"
:style="{
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'center',
marginTop: '20px',
}"
>
<!-- Phosphor tag icon (regular weight) -->
<svg
height="18"
:style="{ width: '18px', height: '18px', marginRight: '8px' }"
viewBox="0 0 256 256"
width="18"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M243.31 136L144 36.69A15.86 15.86 0 0 0 132.69 32H40a8 8 0 0 0-8 8v92.69A15.86 15.86 0 0 0 36.69 144L136 243.31a16 16 0 0 0 22.63 0l84.68-84.68a16 16 0 0 0 0-22.63M148 240L48 140V48h92l100 100ZM96 84a12 12 0 1 1-12-12a12 12 0 0 1 12 12"
fill="#71717a"
/>
</svg>
<span
:style="{
fontSize: '18px',
color: '#71717a',
fontFamily: 'monospace',
}"
>
{{ tags.slice(0, 4).join(' \u00B7 ') }}
</span>
</div>
</div>

<OgImageFooter />
</div>
</template>
25 changes: 25 additions & 0 deletions app/components/OgImage/components/OgImageCategoryLabel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script setup lang="ts">
/**
* Green uppercase category label for OG images (e.g. "Project", "Talk", "Client Project").
* Satori constraints: inline styles only, no Tailwind, hardcoded colors.
*/
defineProps<{
/** The category text to display (e.g. "Project", "Talk"). */
text: string
}>()
</script>

<template>
<div
:style="{
fontSize: '16px',
fontWeight: 600,
color: '#00dc82',
letterSpacing: '0.08em',
textTransform: 'uppercase',
marginBottom: '20px',
}"
>
{{ text }}
</div>
</template>
35 changes: 35 additions & 0 deletions app/components/OgImage/components/OgImageDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup lang="ts">
/**
* Description/subtitle text for OG images.
* Satori constraints: inline styles only, no Tailwind, hardcoded colors.
*/
withDefaults(defineProps<{
/** Description text to display. */
text: string
/** Truncate text to this character count (with ellipsis). 0 = no truncation. */
maxLength?: number
/**
* Font size tier.
* - 'lg' (28px): for Default/Home descriptions.
* - 'md' (24px): for category page descriptions (Project, Talk, Client).
*/
size?: 'lg' | 'md'
}>(), {
maxLength: 0,
size: 'md',
})
</script>

<template>
<div
:style="{
fontSize: size === 'lg' ? '28px' : '24px',
fontWeight: 400,
color: '#a1a1aa',
lineHeight: 1.4,
maxWidth: '800px',
}"
>
{{ maxLength > 0 && text.length > maxLength ? text.slice(0, maxLength - 3) + '...' : text }}
</div>
</template>
88 changes: 88 additions & 0 deletions app/components/content/ProjectCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script setup lang="ts">
const props = defineProps<{
project: {
path: string
name: string
description: string
startDate: string
endDate?: string
liveUrl?: string
repoUrl?: string
repoStars?: number
tags: string[]
}
}>()

/**
* Formats a year or year span for display.
* - Ongoing (no endDate): "2024-present"
* - Same year: "2024"
* - Different years: "2023-2025"
*/
const displayPeriod = computed(() => {
const startYear = props.project.startDate.slice(0, 4)
if (!props.project.endDate) return `${startYear}-present`
const endYear = props.project.endDate.slice(0, 4)
return startYear === endYear ? startYear : `${startYear}-${endYear}`
})
</script>

<template>
<AppCard class="relative cursor-pointer" gap="2" interactive>
<div class="flex items-center gap-4 text-xs">
<span class="flex items-center gap-1 font-mono text-text-dim">
<Icon name="ph:calendar-blank" :size="14" />
{{ displayPeriod }}
</span>
<span v-if="project.repoStars && project.repoStars > 0" class="flex items-center gap-1 font-mono text-text-dim">
<Icon name="ph:star" :size="14" />
{{ project.repoStars }}
</span>
</div>
<h3>
<NuxtLink class="card-link flex items-center gap-2" :to="project.path">
<Icon class="shrink-0" name="ph:folder-open" :size="18" />
{{ project.name }}
</NuxtLink>
</h3>
<div v-if="project.tags.length" class="flex flex-wrap gap-1.5">
<AppTag v-for="tag in project.tags" :key="tag" :label="tag" />
</div>
<p class="text-sm">
{{ project.description }}
</p>
<div v-if="project.liveUrl || project.repoUrl" class="relative z-10 mt-1 flex gap-4">
<NuxtLink
v-if="project.liveUrl"
class="flex items-center gap-1.5 text-xs font-medium text-accent hover:text-accent-hover"
target="_blank"
:to="project.liveUrl"
>
<Icon name="ph:arrow-square-out" :size="16" />
Live
</NuxtLink>
<NuxtLink
v-if="project.repoUrl"
class="flex items-center gap-1.5 text-xs font-medium text-accent hover:text-accent-hover"
target="_blank"
:to="project.repoUrl"
>
<Icon name="ph:github-logo" :size="16" />
Repo
</NuxtLink>
</div>
</AppCard>
</template>

<style scoped>
@reference "~/assets/css/main.css";

.card-link {
@apply text-inherit no-underline;
}

.card-link::after {
@apply absolute inset-0;
content: '';
}
</style>
5 changes: 3 additions & 2 deletions app/components/layout/SiteHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
const links = [
{ to: '/', label: 'About' },
{ to: '/talks', label: 'Talks' },
{ to: '/projects', label: 'Open Source' },
{ to: '/clients', label: 'Client Work' },
{ to: '/projects', label: 'Projects' },
// { to: '/clients', label: 'Client Work' },
// { to: '/publications', label: 'Publications' },
]

const mobileOpen = ref(false)
Expand Down
Loading