portfolio/pages/cv.vue
Michael Winter 711b5b93c2 Clean up data files and fix icon/PDF links
- Clean all JSON data files: convert MongoDB format to clean JSON
  - works.json, images.json, publications.json, pubs.json, talks.json
  - releases.json, album_art.json, scores.json, my_image_gallery.json
  - events.json (with legacy program → program transformation)
  - resume.json (simplified structure)
- Simplify all API routes (remove cleanData functions)
- Fix PDF links to open in new tab (scores, writings, albums)
- Upgrade to Nuxt 4.3.1 and fix carousel (nuxt-swiper)
- Replace nuxt-icon with @nuxt/icon
- Fix IconButton component for new tab links
- Update cv.vue for resume data structure changes
- Add icon collections (@iconify-json packages)
2026-02-18 20:16:09 +01:00

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)
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>
<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, 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>
<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="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>
<!-- 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, idx) in resume.solo_releases" :key="idx" 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, idx) in resume.compilation_releases" :key="idx" 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, 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>
<!-- 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>