Redesign with hamburger menu and separate pages for pieces/writings/albums/performances/lectures
This commit is contained in:
parent
6e9f859d59
commit
97debc4d10
|
|
@ -1,82 +1,94 @@
|
|||
<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">
|
||||
<p class="text-lg">pieces</p>
|
||||
|
||||
<div class="py-2 ml-3" v-for="item in works">
|
||||
<p class="text-sm font-semibold mt-4 text-[#7F7F7F]">{{ item.year }}</p>
|
||||
<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 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>
|
||||
<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>
|
||||
</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 class="px-5">
|
||||
<p class="text-lg">writings</p>
|
||||
|
||||
<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>
|
||||
<span v-html="item.entryTags.title"></span>
|
||||
<div class="ml-4 text-[#7F7F7F]">
|
||||
{{ 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 class="md:col-span-2">
|
||||
<section>
|
||||
<h2 class="text-xl mb-6 border-b border-gray-200 pb-2">albums</h2>
|
||||
|
||||
<div class="grid grid-cols-2 sm:grid-cols-1 gap-6">
|
||||
<div v-for="item in releases" :key="item.title" class="text-center">
|
||||
<p class="italic mb-2">{{ item.title }}</p>
|
||||
<button @click="openAlbumModal(item)" class="block mx-auto">
|
||||
<nuxt-img
|
||||
:src="'/album_art/' + item.album_art"
|
||||
quality="50"
|
||||
class="w-32 sm:w-48"
|
||||
/>
|
||||
</button>
|
||||
<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>
|
||||
<IconButton :visible=item.entryTags.howpublished type="document" :link=item.entryTags.howpublished class="inline-flex p-1 mt-[-6px]"></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
108
components/Menu.vue
Normal file
108
components/Menu.vue
Normal 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>
|
||||
|
|
@ -1,40 +1,34 @@
|
|||
<template>
|
||||
<div class="grid grid-cols-[63%,35%] w-full font-thin sticky top-0 bg-white p-2 z-20">
|
||||
<div>
|
||||
<div class="text-5xl p-2"> <NuxtLink to='/'>michael winter</NuxtLink></div>
|
||||
<div class="inline-flex text-2xl ml-4">
|
||||
<NuxtLink class="px-3 hover:underline" to='/'>works</NuxtLink>
|
||||
<NuxtLink class="px-3 hover:underline" to='/events'>events</NuxtLink>
|
||||
<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 class="min-h-screen bg-white">
|
||||
<header class="sticky top-0 bg-white z-40 border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-center">
|
||||
<h1 class="text-2xl">
|
||||
<NuxtLink to='/' class="hover:underline">michael winter</NuxtLink>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- TODO: this needs to be automatically flipped off when there are no upcoming events-->
|
||||
<!------
|
||||
<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>
|
||||
-->
|
||||
<Menu />
|
||||
|
||||
</div>
|
||||
<slot /> <!-- required here only -->
|
||||
<div class="fixed bottom-0 bg-white p-2 w-full flex justify-center z-20">
|
||||
<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>
|
||||
<main class="max-w-7xl mx-auto px-4 py-8">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
|
|
@ -68,7 +62,8 @@
|
|||
</a>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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>
|
||||
|
|
|
|||
64
pages/albums.vue
Normal file
64
pages/albums.vue
Normal 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>
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
<template>
|
||||
<IndexContent />
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
useHead({
|
||||
titleTemplate: 'Michael Winter - Home / Works - Pieces, Publications, and Albums'
|
||||
})
|
||||
await navigateTo('/pieces', { replace: true })
|
||||
</script>
|
||||
|
|
|
|||
62
pages/lectures.vue
Normal file
62
pages/lectures.vue
Normal 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
76
pages/performances.vue
Normal 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
116
pages/pieces.vue
Normal 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
66
pages/writings.vue
Normal 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>
|
||||
Loading…
Reference in a new issue