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>
|
<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 class="px-5">
|
<div v-for="item in works" :key="item.year" class="mb-6">
|
||||||
<p class="text-lg">pieces</p>
|
<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="py-2 ml-3" v-for="item in works">
|
<div class="flex gap-1">
|
||||||
<p class="text-sm font-semibold mt-4 text-[#7F7F7F]">{{ item.year }}</p>
|
<IconButton v-if="work.score" type="score" :work="work" :link="work.score"></IconButton>
|
||||||
<div class="leading-tight py-1 ml-3" v-for="work in item.works">
|
<IconButton v-if="work.soundcloud_trackid" type="audio" :work="work"></IconButton>
|
||||||
<div class="grid grid-cols-[65%,30%] gap-1 items-start">
|
<IconButton v-if="work.vimeo_trackid" type="video" :work="work"></IconButton>
|
||||||
<NuxtLink v-if="hasItems(work)" :to="'/works/' + slugify(work.title)" class="italic text-sm hover:underline">
|
<IconButton v-if="work.gallery" type="image" :work="work"></IconButton>
|
||||||
<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-2 sm:grid-cols-1 gap-6">
|
||||||
<div class="grid grid-cols-[95%,5%] gap-1 items-start">
|
<div v-for="item in releases" :key="item.title" class="text-center">
|
||||||
<div>
|
<p class="italic mb-2">{{ item.title }}</p>
|
||||||
<span v-html="item.entryTags.title"></span>
|
<button @click="openAlbumModal(item)" class="block mx-auto">
|
||||||
<div class="ml-4 text-[#7F7F7F]">
|
<nuxt-img
|
||||||
{{ item.entryTags.author }}
|
:src="'/album_art/' + item.album_art"
|
||||||
<span v-if=item.entryTags.booktitle>{{ item.entryTags.booktitle}}. </span>
|
quality="50"
|
||||||
<span v-if=item.entryTags.journal>{{item.entryTags.journal}}. </span>
|
class="w-32 sm:w-48"
|
||||||
<span v-if=item.entryTags.editor>editors {{item.entryTags.editor}} </span>
|
/>
|
||||||
<span v-if=item.entryTags.volume>volume {{item.entryTags.volume}}.</span>
|
</button>
|
||||||
<span v-if=item.entryTags.publisher>{{item.entryTags.publisher}}.</span>
|
<div class="flex justify-center gap-2 mt-2">
|
||||||
{{ item.entryTags.year }}.
|
<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
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>
|
<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
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>
|
<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
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