Redesign with hamburger menu and separate pages for pieces/writings/albums/performances/lectures

This commit is contained in:
Michael Winter 2026-03-07 11:28:55 +01:00
parent 6e9f859d59
commit 97debc4d10
9 changed files with 601 additions and 117 deletions

View file

@ -1,82 +1,94 @@
<template> <template>
<div class="bg-zinc-100 rounded-lg m-5 grid grid-cols-3 gap-10 divide-x divide-solid divide-black py-4 mb-10"> <div class="grid grid-cols-1 md:grid-cols-5 gap-8">
<div class="md:col-span-3">
<section class="mb-12">
<h2 class="text-xl mb-6 border-b border-gray-200 pb-2">pieces</h2>
<div v-for="item in works" :key="item.year" class="mb-6">
<p class="text-sm text-gray-500 mb-2">{{ item.year }}</p>
<div v-for="work in item.works" :key="work.title" class="mb-3">
<div class="flex items-start gap-2">
<NuxtLink
v-if="hasItems(work)"
:to="'/works/' + slugify(work.title)"
class="italic hover:underline"
>
<span v-html="work.title"></span>
</NuxtLink>
<span v-else class="italic">
<span v-html="work.title"></span>
</span>
<div class="px-5"> <div class="flex gap-1">
<p class="text-lg">pieces</p> <IconButton v-if="work.score" type="score" :work="work" :link="work.score"></IconButton>
<IconButton v-if="work.soundcloud_trackid" type="audio" :work="work"></IconButton>
<div class="py-2 ml-3" v-for="item in works"> <IconButton v-if="work.vimeo_trackid" type="video" :work="work"></IconButton>
<p class="text-sm font-semibold mt-4 text-[#7F7F7F]">{{ item.year }}</p> <IconButton v-if="work.gallery" type="image" :work="work"></IconButton>
<div class="leading-tight py-1 ml-3" v-for="work in item.works">
<div class="grid grid-cols-[65%,30%] gap-1 items-start">
<NuxtLink v-if="hasItems(work)" :to="'/works/' + slugify(work.title)" class="italic text-sm hover:underline">
<span v-html="work.title"></span>
</NuxtLink>
<span v-else class="italic text-sm">
<span v-html="work.title"></span>
</span>
<div class="inline-flex mt-[-4px]">
<div>
<IconButton :visible="work.score" type="score" :work="work" :link="work.score" class="inline-flex p-1"></IconButton>
</div> </div>
<div>
<IconButton :visible="work.soundcloud_trackid" type="audio" :work="work" class="inline-flex p-1"></IconButton>
</div>
<div>
<IconButton :visible="work.vimeo_trackid" type="video" :work="work" class="inline-flex p-1"></IconButton>
</div>
<div>
<IconButton :visible="work.gallery" type="image" :work="work" class="inline-flex p-1"></IconButton>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </section>
<section>
<h2 class="text-xl mb-6 border-b border-gray-200 pb-2">writings</h2>
<div v-for="item in pubs" :key="item.citationKey" class="mb-4">
<div class="flex items-start gap-2">
<div>
<span v-html="item.entryTags.title" class="italic"></span>
<div class="text-sm text-gray-500">
{{ item.entryTags.author }}
<span v-if="item.entryTags.booktitle">{{ item.entryTags.booktitle }}.</span>
<span v-if="item.entryTags.journal">{{ item.entryTags.journal }}.</span>
<span v-if="item.entryTags.editor">editors {{ item.entryTags.editor }}</span>
<span v-if="item.entryTags.volume">volume {{ item.entryTags.volume }}.</span>
<span v-if="item.entryTags.publisher">{{ item.entryTags.publisher }}.</span>
{{ item.entryTags.year }}.
</div>
</div>
<IconButton
v-if="item.entryTags.howpublished"
type="document"
:link="item.entryTags.howpublished"
></IconButton>
</div>
</div>
</section>
</div> </div>
<div class="px-5"> <div class="md:col-span-2">
<p class="text-lg">writings</p> <section>
<h2 class="text-xl mb-6 border-b border-gray-200 pb-2">albums</h2>
<div class="leading-tight py-2 ml-3 text-sm" v-for="item in pubs">
<div class="grid grid-cols-[95%,5%] gap-1 items-start"> <div class="grid grid-cols-2 sm:grid-cols-1 gap-6">
<div> <div v-for="item in releases" :key="item.title" class="text-center">
<span v-html="item.entryTags.title"></span> <p class="italic mb-2">{{ item.title }}</p>
<div class="ml-4 text-[#7F7F7F]"> <button @click="openAlbumModal(item)" class="block mx-auto">
{{ item.entryTags.author }} <nuxt-img
<span v-if=item.entryTags.booktitle>{{ item.entryTags.booktitle}}.&nbsp;</span> :src="'/album_art/' + item.album_art"
<span v-if=item.entryTags.journal>{{item.entryTags.journal}}.&nbsp;</span> quality="50"
<span v-if=item.entryTags.editor>editors {{item.entryTags.editor}}&nbsp;</span> class="w-32 sm:w-48"
<span v-if=item.entryTags.volume>volume {{item.entryTags.volume}}.</span> />
<span v-if=item.entryTags.publisher>{{item.entryTags.publisher}}.</span> </button>
{{ item.entryTags.year }}. <div class="flex justify-center gap-2 mt-2">
<IconButton
v-if="item.discogs_id"
type="discogs"
:link="'https://www.discogs.com/release/' + item.discogs_id"
:newTab="true"
></IconButton>
<IconButton
v-if="item.buy_link"
type="buy"
:link="item.buy_link"
:newTab="true"
></IconButton>
</div> </div>
</div> </div>
<div>
<IconButton :visible=item.entryTags.howpublished type="document" :link=item.entryTags.howpublished class="inline-flex p-1 mt-[-6px]"></IconButton>
</div>
</div> </div>
</div> </section>
</div> </div>
<div class="px-5">
<p class="text-lg">albums</p>
<div class="flex flex-col items-center leading-tight py-4 text-sm" v-for="item in releases">
<p class="leading-tight py-2">{{ item.title }}</p>
<button @click="openAlbumModal(item)">
<nuxt-img :src="'/album_art/' + item.album_art"
quality="50"/>
</button>
<div class="flex place-content-center place-items-center">
<IconButton :visible="item.discogs_id" type="discogs" :link="'https://www.discogs.com/release/' + item.discogs_id" :newTab="true"></IconButton>
<IconButton :visible="item.buy_link" type="buy" :link="item.buy_link" :newTab="true"></IconButton>
</div>
</div>
</div>
</div> </div>
</template> </template>

108
components/Menu.vue Normal file
View file

@ -0,0 +1,108 @@
<template>
<div>
<button
@click="isOpen = true"
class="fixed top-4 left-4 z-50 p-2 hover:bg-gray-100 rounded"
aria-label="Open menu"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<Transition name="slide">
<div v-if="isOpen" class="fixed inset-0 z-50">
<div class="absolute inset-0 bg-black/50" @click="isOpen = false"></div>
<div class="absolute left-0 top-0 h-full w-64 bg-white shadow-lg p-6 pt-16">
<nav class="space-y-4">
<div>
<p class="text-xs text-gray-400 uppercase tracking-wider mb-2">Works</p>
<div class="space-y-2 ml-2">
<NuxtLink
to="/pieces"
class="block hover:underline"
@click="isOpen = false"
>
pieces
</NuxtLink>
<NuxtLink
to="/writings"
class="block hover:underline"
@click="isOpen = false"
>
writings
</NuxtLink>
<NuxtLink
to="/albums"
class="block hover:underline"
@click="isOpen = false"
>
albums
</NuxtLink>
</div>
</div>
<div>
<p class="text-xs text-gray-400 uppercase tracking-wider mb-2">Events</p>
<div class="space-y-2 ml-2">
<NuxtLink
to="/performances"
class="block hover:underline"
@click="isOpen = false"
>
performances
</NuxtLink>
<NuxtLink
to="/lectures"
class="block hover:underline"
@click="isOpen = false"
>
lectures
</NuxtLink>
</div>
</div>
<NuxtLink
to="/about"
class="block hover:underline"
@click="isOpen = false"
>
about
</NuxtLink>
<a
href="https://unboundedpress.org/code"
target="_blank"
class="block hover:underline"
@click="isOpen = false"
>
code
</a>
</nav>
</div>
</div>
</Transition>
</div>
</template>
<script setup>
const isOpen = ref(false)
const route = useRoute()
watch(() => route.path, () => {
isOpen.value = false
})
</script>
<style scoped>
.slide-enter-active,
.slide-leave-active {
transition: opacity 0.2s ease;
}
.slide-enter-from,
.slide-leave-to {
opacity: 0;
}
</style>

View file

@ -1,40 +1,34 @@
<template> <template>
<div class="grid grid-cols-[63%,35%] w-full font-thin sticky top-0 bg-white p-2 z-20"> <div class="min-h-screen bg-white">
<div> <header class="sticky top-0 bg-white z-40 border-b border-gray-200">
<div class="text-5xl p-2"> <NuxtLink to='/'>michael winter</NuxtLink></div> <div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-center">
<div class="inline-flex text-2xl ml-4"> <h1 class="text-2xl">
<NuxtLink class="px-3 hover:underline" to='/'>works</NuxtLink> <NuxtLink to='/' class="hover:underline">michael winter</NuxtLink>
<NuxtLink class="px-3 hover:underline" to='/events'>events</NuxtLink> </h1>
<NuxtLink class="px-3 hover:underline" to='/about'>about</NuxtLink>
<NuxtLink class="px-3 hover:underline" to='https://unboundedpress.org/code'>code</NuxtLink>
</div>
<!-- hdp link while active -->
<!------
<div class="inline-flex text-2xl ml-4 font-bold">
<NuxtLink class="px-3" to='/a_history_of_the_domino_problem'>A HISTORY OF THE DOMINO PROBLEM | 17.11 - 01.12.2023 </NuxtLink>
</div>
--->
</div> </div>
</header>
<!-- TODO: this needs to be automatically flipped off when there are no upcoming events--> <Menu />
<!------
<div class="px-1 bg-zinc-100 rounded-lg text-center">
<div class="text-sm">upcoming events</div>
<EventSlider :upcoming_events="upcoming_events" class="max-w-[95%] min-h-[80%]"></EventSlider>
</div>
-->
</div> <main class="max-w-7xl mx-auto px-4 py-8">
<slot /> <!-- required here only --> <slot />
<div class="fixed bottom-0 bg-white p-2 w-full flex justify-center z-20"> </main>
<ClientOnly>
<iframe width="400rem" height="20px" scrolling="no" frameborder="no" allow="autoplay" v-if="audioPlayerStore.soundcloud_trackid !== 'undefined'"
:src="'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + audioPlayerStore.soundcloud_trackid + '&inverse=false&auto_play=true&show_user=false'"></iframe>
</ClientOnly>
</div>
<Modal v-model="modalStore.isOpen" :maxHeight="modalStore.type === 'image' && modalStore.soundcloudUrl ? 'calc(85vh + 60px)' : '85vh'"> <footer class="fixed bottom-0 bg-white border-t border-gray-200 p-2 w-full flex justify-center z-30">
<ClientOnly>
<iframe
v-if="audioPlayerStore.soundcloud_trackid !== 'undefined'"
width="400rem"
height="20px"
scrolling="no"
frameborder="no"
allow="autoplay"
:src="'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + audioPlayerStore.soundcloud_trackid + '&inverse=false&auto_play=true&show_user=false'"
></iframe>
</ClientOnly>
</footer>
<Modal v-model="modalStore.isOpen" :maxHeight="modalStore.type === 'image' && modalStore.soundcloudUrl ? 'calc(85vh + 60px)' : '85vh'">
<ModalBody :class="modalStore.aspect"> <ModalBody :class="modalStore.aspect">
<ImageSlider v-if="modalStore.type === 'image'" :bucket="modalStore.bucket" :gallery="modalStore.gallery"></ImageSlider> <ImageSlider v-if="modalStore.type === 'image'" :bucket="modalStore.bucket" :gallery="modalStore.gallery"></ImageSlider>
<div v-if="modalStore.type === 'image' && modalStore.soundcloudUrl" class="flex justify-center mt-2"> <div v-if="modalStore.type === 'image' && modalStore.soundcloudUrl" class="flex justify-center mt-2">
@ -68,7 +62,8 @@
</a> </a>
</div> </div>
</ModalBody> </ModalBody>
</Modal> </Modal>
</div>
</template> </template>
<script setup> <script setup>
@ -96,17 +91,4 @@
}) })
} }
}) })
const { data: upcoming_events } = await useFetch('/api/events', {
transform: (events) => {
const now = new Date().getTime()
const upcoming = events.filter(e => new Date(e.start_date).getTime() >= now)
for (const event of upcoming) {
let date = new Date(event.start_date)
event.formatted_date = ("0" + (date.getMonth() + 1)).slice(-2) + "." + ("0" + date.getDate()).slice(-2) + "." + date.getFullYear()
}
return upcoming.sort((a,b) => new Date(a.start_date) - new Date(b.start_date))
}
})
</script> </script>

64
pages/albums.vue Normal file
View file

@ -0,0 +1,64 @@
<template>
<div>
<section>
<h1 class="text-2xl mb-6 border-b border-gray-200 pb-2">albums</h1>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
<div v-for="item in releases" :key="item.title" class="text-center group">
<button @click="openAlbumModal(item)" class="block w-full relative overflow-hidden">
<nuxt-img
:src="'/album_art/' + item.album_art"
quality="50"
class="w-full aspect-[2/1] object-contain border border-gray-300 transition-opacity duration-200 group-hover:opacity-50"
/>
<p class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 text-center italic px-2">{{ item.title }}</p>
</button>
<div class="flex justify-center gap-4 mt-2 text-sm text-gray-500">
<a
v-if="item.discogs_id"
:href="'https://www.discogs.com/release/' + item.discogs_id"
target="_blank"
class="hover:underline"
>
discogs
</a>
<a
v-if="item.buy_link"
:href="item.buy_link"
target="_blank"
class="hover:underline"
>
buy
</a>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import { useModalStore } from "@/stores/ModalStore"
const modalStore = useModalStore()
const { data: releases } = await useFetch('/api/releases', {
key: 'releases-page',
transform: (releases) => {
const cloned = JSON.parse(JSON.stringify(releases))
return cloned.sort((a,b) => {
const dateA = parseInt(a.date) || 0
const dateB = parseInt(b.date) || 0
return dateB - dateA
})
}
})
const openAlbumModal = (album) => {
modalStore.setModalProps('image', 'aspect-auto', true, 'album_art', [{image: album.album_art}], '')
}
useHead({
titleTemplate: 'Michael Winter - Albums'
})
</script>

View file

@ -1,9 +1,7 @@
<template> <template>
<IndexContent /> <div></div>
</template> </template>
<script setup> <script setup>
useHead({ await navigateTo('/pieces', { replace: true })
titleTemplate: 'Michael Winter - Home / Works - Pieces, Publications, and Albums'
})
</script> </script>

62
pages/lectures.vue Normal file
View file

@ -0,0 +1,62 @@
<template>
<div>
<section>
<h1 class="text-2xl mb-6 border-b border-gray-200 pb-2">lectures</h1>
<div v-for="yearGroup in lecturesByYear" :key="yearGroup.year" class="mb-8">
<p class="text-sm text-gray-500 mb-4">{{ yearGroup.year }}</p>
<div v-for="item in yearGroup.talks" :key="item.date + item.title" class="mb-4">
<div class="text-sm">{{ item.location }}</div>
<div v-for="talk in item.talks" :key="talk.title" class="ml-4 text-sm text-gray-500">
{{ talk.title }}
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
const { data: talksData } = await useFetch('/api/talks', {
transform: (events) => {
for (const event of events) {
let date = new Date(event.date)
event.date = date
event.formatted_date = ("0" + (date.getMonth() + 1)).slice(-2) + "." + date.getFullYear()
if(typeof event.title === 'string' || event.title instanceof String) {
event.talks = [{'title': event.title}]
} else {
let talks = []
for(const talk of event.title){
talks.push({"title": talk})
}
event.talks = talks
}
}
return events.sort((a,b) => new Date(b.date) - new Date(a.date))
}
})
const lecturesByYear = computed(() => {
if (!talksData.value) return []
const byYear = {}
for (const talk of talksData.value) {
const year = talk.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].sort((a, b) => new Date(b.date) - new Date(a.date))
return { year, talks }
})
})
useHead({
titleTemplate: 'Michael Winter - Lectures'
})
</script>

76
pages/performances.vue Normal file
View file

@ -0,0 +1,76 @@
<template>
<div>
<section>
<h1 class="text-2xl mb-6 border-b border-gray-200 pb-2">performances</h1>
<div v-for="yearGroup in eventsByYear" :key="yearGroup.year" class="mb-8">
<p class="text-sm text-gray-500 mb-4">{{ yearGroup.year }}</p>
<div v-for="item in yearGroup.events" :key="item.start_date" class="mb-6">
<div class="flex items-start gap-4">
<div class="text-sm text-gray-400 min-w-[80px]">
{{ item.formatted_date }}
</div>
<div class="flex-1">
<div>
{{ item.venue.city }}, {{ item.venue.state }}
</div>
<div class="text-sm text-gray-500 ml-4">
{{ item.venue.name }}
</div>
<div class="ml-4 mt-2">
<div v-for="performance in item.program" :key="performance.work" class="mb-2">
<div class="italic">{{ performance.work }}</div>
<div v-if="performance.ensemble" class="ml-4 text-sm text-gray-500">
{{ performance.ensemble }}
</div>
<div v-if="performance.performers?.length" class="ml-4 text-sm text-gray-500">
<span v-for="(performer, idx) in performance.performers" :key="performer.name">
<span v-if="idx > 0">, </span>
{{ performer.name }}<span v-if="performer.instrument_tags?.length"> - {{ performer.instrument_tags.join(', ') }}</span>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
const { data: events } = await useFetch('/api/events', {
transform: (events) => {
for (const event of events) {
let date = new Date(event.start_date)
event.formatted_date = ("0" + date.getDate()).slice(-2) + "." + ("0" + (date.getMonth() + 1)).slice(-2) + "." + date.getFullYear()
}
return events.sort((a,b) => new Date(b.start_date) - new Date(a.start_date))
}
})
const eventsByYear = computed(() => {
if (!events.value) return []
const byYear = {}
for (const event of events.value) {
const year = new Date(event.start_date).getFullYear()
if (!byYear[year]) byYear[year] = []
byYear[year].push(event)
}
return Object.keys(byYear)
.sort((a, b) => b - a)
.map(year => ({
year,
events: byYear[year].sort((a, b) => new Date(b.start_date) - new Date(a.start_date))
}))
})
useHead({
titleTemplate: 'Michael Winter - Performances'
})
</script>

116
pages/pieces.vue Normal file
View file

@ -0,0 +1,116 @@
<template>
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
<div class="lg:col-span-3">
<section>
<h1 class="text-2xl mb-6 border-b border-gray-200 pb-2">pieces</h1>
<div v-for="item in works" :key="item.year" class="mb-8">
<p class="text-sm text-gray-500 mb-3">{{ item.year }}</p>
<div v-for="work in item.works" :key="work.title" class="mb-4">
<div class="flex items-start gap-2">
<NuxtLink
v-if="hasItems(work)"
:to="'/works/' + slugify(work.title)"
class="italic hover:underline"
>
<span v-html="work.title"></span>
</NuxtLink>
<span v-else class="italic">
<span v-html="work.title"></span>
</span>
<div class="flex gap-1">
<IconButton v-if="work.score" type="score" :work="work" :link="work.score"></IconButton>
<IconButton v-if="work.soundcloud_trackid" type="audio" :work="work"></IconButton>
<IconButton v-if="work.vimeo_trackid" type="video" :work="work"></IconButton>
<IconButton v-if="work.gallery" type="image" :work="work"></IconButton>
</div>
</div>
</div>
</div>
</section>
</div>
<div class="lg:col-span-2">
<section v-if="worksWithImages.length">
<div class="grid grid-cols-1 gap-4">
<NuxtLink
v-for="work in worksWithImages"
:key="work.title"
:to="'/works/' + slugify(work.title)"
class="block group relative overflow-hidden"
>
<nuxt-img
:src="'/images/' + work.images[0].filename"
:alt="work.title"
class="w-full aspect-[4/3] object-cover transition-opacity duration-200 group-hover:opacity-50"
/>
<p class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 text-center italic px-2" v-html="work.title"></p>
</NuxtLink>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { useModalStore } from "@/stores/ModalStore"
const modalStore = useModalStore()
const route = useRoute()
const slugify = (title) => {
if (!title) return ''
return title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
}
const groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});
const hasItems = (work) => {
return work.vimeo_trackid || (work.images && work.images.length) || work.score
}
const worksWithImages = computed(() => {
if (!works.value) return []
const allWorks = works.value.flatMap(group => group.works)
return allWorks.filter(work => work.images && work.images.length)
})
const { data: works } = await useFetch('/api/works', {
key: 'works-pieces',
transform: (works) => {
const cloned = JSON.parse(JSON.stringify(works))
for (const work of cloned) {
if(work.score){
work.score = "/scores/" + work.score
}
if(work.images){
let gallery = [];
for (const image of work.images){
gallery.push({
image: image.filename,
})
}
work.gallery = gallery
}
}
let priorityGroups = groupBy(cloned, work => work.priority)
let groups = groupBy(priorityGroups["1"], work => new Date(work.date).getFullYear())
groups = Object.keys(groups).map((year) => {
return {
year,
works: groups[year].sort((a,b) => new Date(b.date) - new Date(a.date))
};
});
groups.sort((a,b) => b.year - a.year)
if (priorityGroups["2"]) {
groups.push({year: "miscellany", works: priorityGroups["2"].sort((a,b) => new Date(b.date) - new Date(a.date))})
}
return groups
}
})
useHead({
titleTemplate: 'Michael Winter - Pieces'
})
</script>

66
pages/writings.vue Normal file
View file

@ -0,0 +1,66 @@
<template>
<div>
<section>
<h1 class="text-2xl mb-6 border-b border-gray-200 pb-2">writings</h1>
<div v-for="item in pubs" :key="item.citationKey" class="mb-6">
<div class="flex items-start gap-2">
<div>
<a
v-if="item.entryTags.howpublished"
:href="item.entryTags.howpublished"
target="_blank"
class="hover:underline"
>
<span v-html="item.entryTags.title"></span>
</a>
<span v-else v-html="item.entryTags.title"></span>
<div class="text-sm text-gray-500 ml-4">
{{ item.entryTags.author }}
<span v-if="item.entryTags.booktitle">{{ item.entryTags.booktitle }}.</span>
<span v-if="item.entryTags.journal">{{ item.entryTags.journal }}.</span>
<span v-if="item.entryTags.editor">editors {{ item.entryTags.editor }}</span>
<span v-if="item.entryTags.volume">volume {{ item.entryTags.volume }}.</span>
<span v-if="item.entryTags.publisher">{{ item.entryTags.publisher }}.</span>
{{ item.entryTags.year }}.
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
const isValidUrl = urlString => {
var pattern = /^((http|https|ftp):\/\/)/;
return pattern.test(urlString)
}
const { data: pubs } = await useFetch('/api/publications', {
key: 'publications-page',
transform: (pubs) => {
const cloned = JSON.parse(JSON.stringify(pubs))
for (const pub of cloned) {
if(pub.entryTags && pub.entryTags.howpublished && !(isValidUrl(pub.entryTags.howpublished))){
pub.entryTags.howpublished = "/pubs/" + pub.entryTags.howpublished
}
}
return cloned.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))
})
}
})
useHead({
titleTemplate: 'Michael Winter - Writings'
})
</script>