portfolio/pages/cv.vue

487 lines
13 KiB
Vue
Raw Normal View History

<script setup>
definePageMeta({
layout: 'plain'
})
const { data: resumeData } = await useFetch('/api/resume')
const { data: talksData } = await useFetch('/api/talks')
const resume = computed(() => resumeData.value)
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 }
})
})
const sortedPublications = computed(() => {
if (!resumeData.value?.publications) return []
return [...resumeData.value.publications].sort((a, b) => {
const getYear = (key) => parseInt(key.replace(/\D/g, '')) || 0
const getSuffix = (key) => key.replace(/^Winter\d+/, '') || ''
const yearA = getYear(a.citationKey)
const yearB = getYear(b.citationKey)
if (yearA !== yearB) return yearB - yearA
return getSuffix(b.citationKey).localeCompare(getSuffix(a.citationKey))
})
})
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, idx) in resume.education" :key="idx" 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, idx) in resume.teaching" :key="idx" 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>
<template v-if="teach.highlights || teach.courses">
<div class="item-detail ml-4 hanging-indent">
<strong>Subjects and courses:</strong>{{ ' ' }}<template v-if="teach.highlights">
<template v-for="h in teach.highlights">{{ h.startsWith('Topics:') ? h.replace('Topics: ', '') : '' }}</template>
</template><template v-if="teach.highlights && teach.courses">, </template><template v-if="teach.courses">{{ teach.courses.join(', ') }}</template>
</div>
<template v-for="h in teach.highlights">
<div v-if="h.startsWith && h.startsWith('Activities')" class="item-detail ml-4 hanging-indent">
<strong>Activities and Responsibilities:</strong> {{ h.replace(/^Activities( and Responsibilities)?: /, '') }}
</div>
<div v-else-if="h.startsWith && !h.startsWith('Topics:')" class="item-detail ml-4 hanging-indent">
{{ h }}
</div>
</template>
</template>
<div v-else-if="teach.summary" class="item-detail ml-4 hanging-indent">
{{ teach.summary }}
</div>
<div v-if="teach.committees" class="item-detail ml-4 hanging-indent">
<strong>Committees:</strong> {{ teach.committees.map(c => c.role + ' - ' + c.name).join('; ') }}
</div>
<div v-if="teach.events" class="item-detail ml-4 hanging-indent">
<strong>Events:</strong> {{ teach.events.map(e => e.name + ' (' + e.type + ')').join(', ') }}
</div>
</div>
</div>
</section>
<!-- Publications -->
<section v-if="resume?.publications?.length" class="cv-section">
<h4>Publications</h4>
<div class="cv-entry">
<div v-for="pub in sortedPublications" :key="pub.id" class="item">
<div class="item-title" v-html="pub.entryTags?.title"></div>
<div class="bib ml-4 text-[#7F7F7F]">
{{ pub.entryTags?.author }}
<span v-if="pub.entryTags?.booktitle">{{ pub.entryTags?.booktitle }}.&nbsp;</span>
<span v-if="pub.entryTags?.journal">{{ pub.entryTags?.journal }}.&nbsp;</span>
<span v-if="pub.entryTags?.editor">editors {{ pub.entryTags?.editor }}&nbsp;</span>
<span v-if="pub.entryTags?.volume">volume {{ pub.entryTags?.volume }}.</span>
<span v-if="pub.entryTags?.publisher">{{ pub.entryTags?.publisher }}.</span>
{{ pub.entryTags?.year }}.
</div>
</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, idx) in resume.work" :key="idx" 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>
<div v-if="w.summary" class="item-detail ml-4 hanging-indent">
<strong>Project:</strong> {{ w.summary.replace('Project: ', '') }}
</div>
<div v-if="w.highlights" class="ml-4">
<div v-for="h in w.highlights" class="item-detail hanging-indent">
<span v-if="h.startsWith('Activities')"><strong>Activities and Responsibilities:</strong> {{ h.replace(/^Activities( and Responsibilities)?: /, '') }}</span>
<span v-else>{{ h }}</span>
</div>
</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, idx) in resume.solo_releases" :key="idx" class="item recording-item">
<span class="item-title">{{ rel.title }}</span>
<span class="item-detail ml-4">{{ 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, idx) in resume.compilation_releases" :key="idx" class="item recording-item">
<span class="item-title">{{ rel.title }}</span>
<span class="item-detail ml-4">{{ rel.publisher }}. {{ rel.media_type }}. {{ rel.date }}.</span>
<div class="item-detail ml-4">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, idx) in resume.residencies" :key="idx" class="item">
<span class="item-title">{{ res.org }}</span>
<span class="item-meta">{{ res.date }}</span>
</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="idx">
{{ 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="idx">
{{ lang.language }} {{ lang.fluency }}{{ idx < resume.languages.length - 1 ? '; ' : '' }}
</span>
</p>
</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;
}
.hanging-indent {
padding-left: 1rem;
text-indent: -1rem;
}
.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>