portfolio/pages/cv.vue

438 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
definePageMeta({
layout: 'plain'
})
const { data: resumeData } = await useFetch('/api/resume')
const { data: talksData } = await useFetch('/api/talks')
const resume = computed(() => resumeData.value?.[0])
const talksByYear = computed(() => {
if (!talksData.value) return []
const byYear = {}
for (const talk of talksData.value) {
const year = talk.date ? new Date(talk.date).getFullYear() : 'Unknown'
if (!byYear[year]) byYear[year] = []
byYear[year].push(talk)
}
return Object.keys(byYear)
.sort((a, b) => b - a)
.map(year => {
const talks = byYear[year]
const byLocation = {}
for (const talk of talks) {
const key = `${talk.location}|||${talk.date}`
if (!byLocation[key]) byLocation[key] = []
byLocation[key].push(talk)
}
const groups = Object.values(byLocation).map(group => ({
location: group[0].location,
date: group[0].date,
titles: group.map(t => t.title)
}))
return { year, groups }
})
})
function formatMonth(dateStr) {
if (!dateStr) return 'Present'
const date = new Date(dateStr)
if (isNaN(date)) return dateStr
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
}
function formatYear(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
if (isNaN(date)) return dateStr
return date.getFullYear()
}
useHead({
titleTemplate: 'Michael Winter'
})
</script>
<template>
<div class="cv-container">
<header class="cv-header">
<h1>Michael Winter</h1>
<h3>Curriculum Vitae</h3>
<p class="contact">
{{ resume?.basics?.email }} &nbsp;·&nbsp; {{ resume?.basics?.phone }} &nbsp;·&nbsp; {{ resume?.basics?.website }}
</p>
</header>
<hr />
<!-- Education -->
<section v-if="resume?.education?.length" class="cv-section">
<h4>Education</h4>
<div class="cv-entry">
<div v-for="edu in resume.education" :key="edu.id" class="item">
<span class="item-title">{{ edu.studyType }} in {{ edu.area }}</span>
<span class="item-meta">{{ edu.institution }}, {{ formatYear(edu.endDate) }}</span>
</div>
</div>
</section>
<!-- Teaching -->
<section v-if="resume?.teaching?.length" class="cv-section">
<h4>Teaching</h4>
<div class="cv-entry">
<div v-for="teach in resume.teaching" :key="teach.id" class="item">
<div class="item-header">
<span class="item-title">{{ teach.company }}</span>
<span class="item-subtitle">{{ teach.position }}</span>
</div>
<div class="item-detail">{{ formatMonth(teach.startDate) }} {{ formatMonth(teach.endDate) }}</div>
<ul v-if="teach.highlights" class="item-list">
<li v-for="h in teach.highlights">{{ h }}</li>
</ul>
</div>
</div>
</section>
<!-- Lectures -->
<section v-if="talksByYear.length" class="cv-section">
<h4>Lectures</h4>
<div v-for="yearGroup in talksByYear" :key="yearGroup.year" class="year-group">
<div class="year-header">{{ yearGroup.year }}</div>
<div v-for="(group, idx) in yearGroup.groups" :key="idx" class="item">
<span class="item-title">{{ group.location }}</span>
<template v-for="(title, tidx) in group.titles" :key="tidx">
<div class="item-detail talk-title" v-if="Array.isArray(title)">
<em v-for="(t, i) in title" :key="i" style="display: block;">{{ t }}</em>
</div>
<div class="item-detail talk-title" v-else>
<em style="display: block;">{{ title }}</em>
</div>
</template>
</div>
</div>
</section>
<!-- Relevant Work -->
<section v-if="resume?.work?.length" class="cv-section">
<h4>Relevant Work</h4>
<div class="cv-entry">
<div v-for="w in resume.work" :key="w.id" class="item">
<div class="item-header">
<span class="item-title">{{ w.company }}</span>
<span class="item-subtitle">{{ w.position }}</span>
</div>
<div class="item-detail">{{ formatMonth(w.startDate) }} {{ formatMonth(w.endDate) }}</div>
<ul v-if="w.highlights" class="item-list">
<li v-for="h in w.highlights">{{ h }}</li>
</ul>
</div>
</div>
</section>
<!-- Skills -->
<section v-if="resume?.skills?.length" class="cv-section">
<h4>Coding Skills</h4>
<p class="item-detail">
<span v-for="(skill, idx) in resume.skills" :key="skill.id">
{{ skill.keywords?.join(', ') }}
</span>
</p>
</section>
<!-- Languages -->
<section v-if="resume?.languages?.length" class="cv-section">
<h4>Language Skills</h4>
<p class="item-detail">
<span v-for="(lang, idx) in resume.languages" :key="lang.id">
{{ lang.language }} — {{ lang.fluency }}{{ idx < resume.languages.length - 1 ? '; ' : '' }}
</span>
</p>
</section>
<!-- Publications -->
<section v-if="resume?.publications?.length" class="cv-section">
<h4>Publications</h4>
<div class="cv-entry">
<div v-for="pub in resume.publications" :key="pub.id" class="item">
<div class="item-title" v-html="pub.entryTags?.title"></div>
<div class="bib">
{{ pub.entryTags?.author }}
<span v-if="pub.entryTags?.editor">, editors {{ pub.entryTags.editor }}.</span>
<span v-if="pub.entryTags?.booktitle"><em>{{ pub.entryTags.booktitle }}.</em></span>
<span v-if="pub.entryTags?.journal"><em>{{ pub.entryTags.journal }}</em>,</span>
<span v-if="pub.entryTags?.volume">vol. {{ pub.entryTags.volume }}</span>
<span v-if="pub.entryTags?.publisher">{{ pub.entryTags.publisher }},</span> {{ pub.entryTags?.year }}.
</div>
</div>
</div>
</section>
<!-- Recordings -->
<section v-if="resume?.solo_releases?.length || resume?.compilation_releases?.length" class="cv-section">
<h4>Recordings</h4>
<div v-if="resume?.solo_releases?.length" class="subsection">
<div class="subsection-title"><strong>Solo Albums</strong></div>
<div v-for="rel in resume.solo_releases" :key="rel.id" class="item recording-item">
<span class="item-title">{{ rel.title }}</span>
<span class="item-detail">{{ rel.publisher }}. {{ rel.media_type }}. {{ rel.date }}.</span>
</div>
</div>
<div v-if="resume?.compilation_releases?.length" class="subsection">
<div class="subsection-title"><strong>Compilation Albums</strong></div>
<div v-for="rel in resume.compilation_releases" :key="rel.id" class="item recording-item">
<span class="item-title">{{ rel.title }}</span>
<span class="item-detail">{{ rel.publisher }}. {{ rel.media_type }}. {{ rel.date }}.</span>
<div class="item-detail">featuring <span class="italic">{{ rel.work }}</span></div>
</div>
</div>
</section>
<!-- Residencies -->
<section v-if="resume?.residencies?.length" class="cv-section">
<h4>Residencies and Awards</h4>
<div class="cv-entry">
<div v-for="res in resume.residencies" :key="res.id" class="item">
<span class="item-title">{{ res.org }}</span>
<span class="item-meta">{{ res.date }}</span>
</div>
</div>
</section>
<!-- References -->
<section v-if="resume?.references?.length" class="cv-section">
<h4>References</h4>
<div class="cv-entry">
<div v-for="ref in resume.references" :key="ref.id" class="item">
<span class="item-title">{{ ref.name }}</span>
<span class="item-detail">{{ ref.position }}</span>
<span class="item-detail">{{ ref.email }}</span>
</div>
</div>
</section>
</div>
</template>
<style>
.cv-container {
font-size: 12px;
width: 175mm;
margin: 40px auto;
max-width: 100%;
padding: 0 30px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.5;
color: #222;
}
.cv-header {
text-align: center;
margin-bottom: 24px;
}
.cv-header h1 {
font-size: 28px;
font-weight: 600;
margin: 0 0 4px 0;
letter-spacing: -0.5px;
}
.cv-header h3 {
font-size: 16px;
font-weight: 400;
margin: 0 0 8px 0;
color: #555;
}
.cv-header .contact {
font-size: 11px;
color: #555;
margin: 0;
}
.cv-section {
margin-bottom: 20px;
}
.cv-section h4 {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.8px;
margin: 0 0 10px 0;
padding-bottom: 4px;
border-bottom: 1px solid #ccc;
color: #222;
}
.cv-entry {
margin-left: 10px;
}
.item {
margin-bottom: 8px;
}
.item-header {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: baseline;
}
.item-title {
font-weight: 500;
}
.item-subtitle {
font-style: italic;
color: #444;
}
.item-meta {
font-size: 11px;
color: #555;
margin-left: 6px;
}
.item-detail {
font-size: 11px;
color: #555;
display: block;
margin-top: 2px;
}
.talk-title {
margin-left: 12px;
}
.item-list {
margin: 4px 0 0 0;
padding-left: 16px;
font-size: 11px;
color: #444;
}
.item-list li {
margin-bottom: 2px;
}
.year-group {
margin-bottom: 12px;
margin-left: 10px;
}
.year-header {
font-size: 12px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
}
.subsection {
margin-top: 10px;
margin-left: 10px;
}
.subsection-title {
font-size: 11px;
font-weight: bold;
color: #444;
margin-bottom: 6px;
}
.recording-item {
margin-left: 12px;
}
.italic {
font-style: italic;
}
.bib {
font-size: 11px;
color: #444;
margin-top: 2px;
}
hr {
margin: 16px 0;
border: none;
border-top: 1px solid #ccc;
}
a {
color: #222;
text-decoration: underline;
}
@media print {
@page {
margin: 15mm;
}
.cv-container {
margin: 0;
padding: 15mm;
width: auto;
font-size: 10pt;
max-width: none;
box-sizing: border-box;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.cv-header h1 {
font-size: 20pt;
}
.cv-header h3 {
font-size: 12pt;
}
.cv-section h4 {
font-size: 10pt;
border-bottom: 1pt solid #999;
break-after: avoid;
page-break-after: avoid;
}
.item {
break-inside: avoid;
page-break-inside: avoid;
}
.item-title {
font-weight: 500;
}
.item-meta,
.item-detail,
.item-list {
font-size: 9pt;
}
.year-header {
font-size: 10pt;
break-after: avoid;
page-break-after: avoid;
}
hr {
border-top: 1pt solid #999;
}
.cv-entry,
.year-group,
.subsection {
margin-left: 0;
}
}
</style>