feat: add hub

This commit is contained in:
2026-10-03 23:57:41 +02:00
parent 5452f72087
commit fb7b560696
52 changed files with 290 additions and 162 deletions
+4 -1
View File
@@ -13,7 +13,10 @@ export default defineAppConfig({
/** Printed with the date at the bottom of the PDF, e.g. "Berlin, 02.10.2026" */
city: 'Berlin',
linkedin: 'https://linkedin.com/in/joaquingottlebe',
/** Accent color of the CV page (links, active language); the rest of the site keeps its primary color */
/** Accent color of the CV page in light mode and in the PDF (links, active language); dark mode uses the site's primary color */
accent: 'rgb(53 116 110)'
},
hub: {
cv: { title: 'Lebenslauf', icon: 'i-lucide-file-user', to: '/cv' }
}
})
+6
View File
@@ -38,10 +38,13 @@ const { lang, t } = useCvLang()
target="_blank"
:icon="link.kind === 'github' ? 'i-lucide-github' : 'i-lucide-globe'"
:label="link.label"
:title="link.label"
:aria-label="link.label"
color="primary"
variant="link"
size="sm"
class="p-0 print:hidden"
:ui="CV_LINK_UI"
/>
<a
v-for="link in group"
@@ -58,10 +61,13 @@ const { lang, t } = useCvLang()
target="_blank"
icon="i-lucide-file-down"
:label="file.name"
:title="file.name"
:aria-label="file.name"
color="primary"
variant="link"
size="sm"
class="p-0"
:ui="CV_LINK_UI"
/>
</li>
</ul>
+46 -36
View File
@@ -1,7 +1,4 @@
<script setup lang="ts">
// Always light: white background, also when the device is in dark mode
definePageMeta({ colorMode: 'light' })
const { cv: profile } = useAppConfig()
const { lang, t } = useCvLang()
@@ -33,10 +30,11 @@ const experience = computed(() => entries.value.filter(entry => !EDUCATION.inclu
</script>
<template>
<UMain :style="{ '--ui-primary': profile.accent }">
<UMain class="cv" :style="{ '--cv-accent': profile.accent }">
<UContainer class="max-w-3xl py-8 print:max-w-none print:px-[12mm] print:pt-[15mm] print:pb-0">
<div class="mb-6 flex justify-end print:hidden">
<UFieldGroup size="xs">
<div class="mb-6 flex justify-end gap-2 print:hidden">
<AppHeaderActions />
<UFieldGroup>
<UButton
v-for="option in (['de', 'en'] as const)"
:key="option"
@@ -49,36 +47,43 @@ const experience = computed(() => entries.value.filter(entry => !EDUCATION.inclu
</UFieldGroup>
</div>
<header class="mb-10 flex items-center gap-5">
<UAvatar v-if="profile.photo" :src="profile.photo" :alt="profile.name || t.photo" class="size-20 shrink-0" />
<div class="min-w-0">
<h1 class="text-2xl font-semibold text-highlighted">
{{ profile.name || 'CV' }}
</h1>
<p v-if="profile.description[lang]" class="mt-1 text-sm text-muted">
{{ profile.description[lang] }}
</p>
<div class="mt-1 flex flex-wrap gap-x-4">
<UButton
v-if="profile.email"
:to="`mailto:${profile.email}`"
:label="profile.email"
color="primary"
variant="link"
class="p-0"
/>
<UButton
v-if="profile.linkedin"
:to="profile.linkedin"
target="_blank"
label="LinkedIn"
color="primary"
variant="link"
class="p-0 print:hidden"
/>
<!-- In the PDF links are written out, since the label alone can't be followed on paper -->
<a v-if="profile.linkedin" :href="profile.linkedin" class="hidden text-sm font-medium text-primary print:inline">{{ cvLinkText(profile.linkedin) }}</a>
</div>
<header class="mb-10">
<UAvatar v-if="profile.photo" :src="profile.photo" :alt="profile.name || t.photo" class="mb-4 size-20" />
<!-- Under the photo: name, description, links -->
<h1 class="text-2xl font-semibold text-highlighted">
{{ profile.name || 'CV' }}
</h1>
<p v-if="profile.description[lang]" class="mt-1 text-sm text-muted">
{{ profile.description[lang] }}
</p>
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-1">
<UButton
v-if="profile.email"
:to="`mailto:${profile.email}`"
icon="i-lucide-mail"
:label="profile.email"
:title="profile.email"
:aria-label="profile.email"
color="primary"
variant="link"
class="p-0"
:ui="CV_LINK_UI"
/>
<UButton
v-if="profile.linkedin"
:to="profile.linkedin"
target="_blank"
icon="i-lucide-linkedin"
label="LinkedIn"
title="LinkedIn"
aria-label="LinkedIn"
color="primary"
variant="link"
class="p-0 print:hidden"
:ui="CV_LINK_UI"
/>
<!-- In the PDF links are written out, since the label alone can't be followed on paper -->
<a v-if="profile.linkedin" :href="profile.linkedin" class="hidden text-sm font-medium text-primary print:inline">{{ cvLinkText(profile.linkedin) }}</a>
</div>
</header>
@@ -109,6 +114,11 @@ const experience = computed(() => entries.value.filter(entry => !EDUCATION.inclu
</template>
<style>
/* The CV's accent instead of the site's primary color in light mode (and so in the PDF); dark mode keeps the site's */
html:not(.dark) .cv {
--ui-primary: var(--cv-accent);
}
/* Top and bottom of every printed page. The sides and the extra space above the first page come from the container's
print padding, which also works in browsers that ignore @page margins */
@page {
+3
View File
@@ -2,3 +2,6 @@
export function cvLinkText(url: string): string {
return url.replace(/^https?:\/\//, '').replace(/\/$/, '')
}
/** Link buttons show only their icon on phones, where long labels don't fit; the label is then the tooltip and aria-label. The PDF shows the text without the icon (only the email is printed this way) */
export const CV_LINK_UI = { label: 'hidden sm:inline print:inline', leadingIcon: 'print:hidden' }