portfolio/pages/cv.vue

472 lines
13 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)
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, 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 resume.publications" :key="pub.id" class="item">
<div class="item-title" v-html="pub.entryTags?.title"></div>
<div class="bib ml-4">
<span v-if="pub.entryTags?.author">{{ pub.entryTags.author }}.</span>
<span v-if="pub.entryTags?.editor">
<span v-if="pub.entryTags?.author">, </span>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>
<!-- 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>