Compare commits
10 commits
6e9f859d59
...
29dace6fc8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29dace6fc8 | ||
|
|
1694c4a890 | ||
|
|
0c66aecda8 | ||
|
|
e4e733d87a | ||
|
|
b44c9c324a | ||
|
|
8bb72b5d48 | ||
|
|
f0133650f6 | ||
|
|
69fab67601 | ||
|
|
be041de733 | ||
|
|
97debc4d10 |
2
app.vue
2
app.vue
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-white min-w-[800px] min-h-[80vh]">
|
<div class="bg-white min-h-[80vh]">
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
<NuxtPage/>
|
<NuxtPage/>
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
|
|
|
||||||
|
|
@ -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}}. </span>
|
:src="'/album_art/' + item.album_art"
|
||||||
<span v-if=item.entryTags.journal>{{item.entryTags.journal}}. </span>
|
quality="50"
|
||||||
<span v-if=item.entryTags.editor>editors {{item.entryTags.editor}} </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>
|
||||||
|
|
||||||
|
|
|
||||||
112
components/Menu.vue
Normal file
112
components/Menu.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
@click="isOpen = true"
|
||||||
|
class="flex-shrink-0 hover:bg-gray-100 rounded mt-2"
|
||||||
|
aria-label="Open menu"
|
||||||
|
>
|
||||||
|
<svg class="w-7 h-7" 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">
|
||||||
|
<NuxtLink to="/" class="block text-xl hover:underline" @click="isOpen = false">
|
||||||
|
michael winter
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<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,45 +1,78 @@
|
||||||
<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>
|
<!-- Mobile header -->
|
||||||
<div class="text-5xl p-2"> <NuxtLink to='/'>michael winter</NuxtLink></div>
|
<header class="sticky top-0 bg-white z-40 border-b border-gray-200 lg:hidden">
|
||||||
<div class="inline-flex text-2xl ml-4">
|
<div class="max-w-7xl mx-auto px-4 py-2 flex items-center justify-between gap-4">
|
||||||
<NuxtLink class="px-3 hover:underline" to='/'>works</NuxtLink>
|
<Menu />
|
||||||
<NuxtLink class="px-3 hover:underline" to='/events'>events</NuxtLink>
|
<h1 class="text-lg md:text-2xl whitespace-nowrap">
|
||||||
<NuxtLink class="px-3 hover:underline" to='/about'>about</NuxtLink>
|
<NuxtLink to='/' class="hover:underline">michael winter</NuxtLink>
|
||||||
<NuxtLink class="px-3 hover:underline" to='https://unboundedpress.org/code'>code</NuxtLink>
|
</h1>
|
||||||
</div>
|
<div class="flex-1 text-right text-lg md:text-xl">{{ pageTitle }}</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-->
|
<!-- Desktop header -->
|
||||||
<!------
|
<header class="sticky top-0 bg-white z-40 border-b border-gray-200 hidden lg:block">
|
||||||
<div class="px-1 bg-zinc-100 rounded-lg text-center">
|
<div class="w-full px-4 py-2 flex items-center justify-between">
|
||||||
<div class="text-sm">upcoming events</div>
|
<h1 class="text-lg md:text-2xl whitespace-nowrap">
|
||||||
<EventSlider :upcoming_events="upcoming_events" class="max-w-[95%] min-h-[80%]"></EventSlider>
|
<NuxtLink to='/' class="hover:underline">michael winter</NuxtLink>
|
||||||
|
</h1>
|
||||||
|
<div class="text-lg md:text-xl">{{ pageTitle }}</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
</header>
|
||||||
|
|
||||||
</div>
|
<div class="flex">
|
||||||
<slot /> <!-- required here only -->
|
<!-- Sidebar for wide screens -->
|
||||||
<div class="fixed bottom-0 bg-white p-2 w-full flex justify-center z-20">
|
<aside class="hidden lg:block w-48 flex-shrink-0 border-r border-gray-200 p-4 pt-12 sticky self-start top-12 z-20">
|
||||||
<ClientOnly>
|
<nav class="space-y-4 ml-4">
|
||||||
<iframe width="400rem" height="20px" scrolling="no" frameborder="no" allow="autoplay" v-if="audioPlayerStore.soundcloud_trackid !== 'undefined'"
|
<div>
|
||||||
: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>
|
<p class="text-xs text-gray-400 uppercase tracking-wider mb-2">Works</p>
|
||||||
</ClientOnly>
|
<div class="space-y-2 ml-2">
|
||||||
</div>
|
<NuxtLink to="/pieces" class="block hover:underline">pieces</NuxtLink>
|
||||||
|
<NuxtLink to="/writings" class="block hover:underline">writings</NuxtLink>
|
||||||
|
<NuxtLink to="/albums" class="block hover:underline">albums</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Modal v-model="modalStore.isOpen" :maxHeight="modalStore.type === 'image' && modalStore.soundcloudUrl ? 'calc(85vh + 60px)' : '85vh'">
|
<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">performances</NuxtLink>
|
||||||
|
<NuxtLink to="/lectures" class="block hover:underline">lectures</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NuxtLink to="/about" class="block hover:underline">about</NuxtLink>
|
||||||
|
<a href="https://unboundedpress.org/code" target="_blank" class="block hover:underline">code</a>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<main class="max-w-7xl mx-auto px-4 py-8">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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'"
|
||||||
|
class="w-64"
|
||||||
|
height="20px"
|
||||||
|
scrolling="no"
|
||||||
|
frameborder="no"
|
||||||
|
: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">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<iframe :src="modalStore.soundcloudUrl" width="400rem" height="20px" scrolling="no" frameborder="no" allow="autoplay"></iframe>
|
<iframe :src="modalStore.soundcloudUrl" class="w-64" height="20px" scrolling="no" frameborder="no" allow="autoplay"></iframe>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="modalStore.type === 'video'" :class="modalStore.aspect" class="w-full h-full flex items-center justify-center p-4">
|
<div v-if="modalStore.type === 'video'" :class="modalStore.aspect" class="w-full h-full flex items-center justify-center p-4">
|
||||||
|
|
@ -58,7 +91,7 @@
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
<div v-if="modalStore.soundcloudUrl" class="flex justify-center mt-2">
|
<div v-if="modalStore.soundcloudUrl" class="flex justify-center mt-2">
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<iframe :src="modalStore.soundcloudUrl" width="400rem" height="20px" scrolling="no" frameborder="no" allow="autoplay"></iframe>
|
<iframe :src="modalStore.soundcloudUrl" class="w-64" height="20px" scrolling="no" frameborder="no" allow="autoplay"></iframe>
|
||||||
</ClientOnly>
|
</ClientOnly>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -68,19 +101,32 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
||||||
import { useModalStore } from "@/stores/ModalStore"
|
import { useModalStore } from "@/stores/ModalStore"
|
||||||
import { onMounted } from "vue"
|
import { onMounted, computed } from "vue"
|
||||||
|
|
||||||
const audioPlayerStore = useAudioPlayerStore()
|
const audioPlayerStore = useAudioPlayerStore()
|
||||||
const modalStore = useModalStore()
|
const modalStore = useModalStore()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const pageTitle = computed(() => {
|
||||||
|
const path = route.path
|
||||||
|
if (path === '/' || path === '/pieces') return 'pieces'
|
||||||
|
if (path === '/writings') return 'writings'
|
||||||
|
if (path === '/albums') return 'albums'
|
||||||
|
if (path === '/performances') return 'performances'
|
||||||
|
if (path === '/lectures') return 'lectures'
|
||||||
|
if (path === '/about') return 'about'
|
||||||
|
if (path.startsWith('/works/')) return 'works'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if(route.params.files == 'scores') {
|
if(route.params.files == 'scores') {
|
||||||
useFetch('/api/works', {
|
useFetch('/api/works', {
|
||||||
|
|
@ -96,17 +142,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>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routeRules: {
|
routeRules: {
|
||||||
|
'/': { redirect: '/pieces' },
|
||||||
'/hdp': { redirect: '/a_history_of_the_domino_problem' },
|
'/hdp': { redirect: '/a_history_of_the_domino_problem' },
|
||||||
|
|
||||||
// Default: prerender all routes (static HTML)
|
// Default: prerender all routes (static HTML)
|
||||||
|
|
|
||||||
163
pages/about.vue
163
pages/about.vue
|
|
@ -1,91 +1,98 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-zinc-100 rounded-lg m-5 grid grid-cols-[60%,35%] gap-10 divide-x divide-solid divide-black py-4 min-h-[calc(100vh-10.5rem)]">
|
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
||||||
<div class="px-5">
|
<div class="lg:col-span-3">
|
||||||
<p class="text-lg">about</p>
|
<section>
|
||||||
|
<div class="mb-8">
|
||||||
<div class="leading-tight py-2 ml-3 text-sm">
|
<p class="mb-4">
|
||||||
<div class="leading-tight py-2">
|
My practice as a composer and sound artist is diverse, ranging from music created by digital and acoustic instruments to installations and kinetic sculptures. Each piece typically explores one simple process and often reflects various related interests of mine such as epistemology, mathematics, algorithmic information theory, and the history of science. Phenomenologically, I contemplate the possibility that everything is potentially computable, even our experiences. Given this digital philosophy, I acknowledge even my most open works as algorithmic; and, while not always apparent on the surface of any given piece, the considerations of computability and epistemology are integral to my practice. I often reconcile epistemological limits with artistic practicality by understanding the limits of computation from an artistic and experiential vantage point and by collaborating with other artists, mathematicians, and scientists in order to integrate objects, ideas, and texts from various domains as structural elements in my pieces. My work also aims to subvert discriminatory conventions and hierarchies by exploring alternative forms of presentation and interaction, often with minimal resources and low information.
|
||||||
My practice as a composer and sound artist is diverse, ranging from music created by digital and acoustic instruments to installations and kinetic sculptures. Each piece typically explores one simple process and often reflects various related interests of mine such as epistemology, mathematics, algorithmic information theory, and the history of science. Phenomenologically, I contemplate the possibility that everything is potentially computable, even our experiences. Given this digital philosophy, I acknowledge even my most open works as algorithmic; and, while not always apparent on the surface of any given piece, the considerations of computability and epistemology are integral to my practice. I often reconcile epistemological limits with artistic practicality by understanding the limits of computation from an artistic and experiential vantage point and by collaborating with other artists, mathematicians, and scientists in order to integrate objects, ideas, and texts from various domains as structural elements in my pieces. My work also aims to subvert discriminatory conventions and hierarchies by exploring alternative forms of presentation and interaction, often with minimal resources and low information.
|
</p>
|
||||||
</div>
|
<p>
|
||||||
<div class="leading-tight py-2">
|
My music and installations have been presented at venues and festivals throughout the world such as REDCAT, in Los Angeles; the Ostrava Festival of New Music in the Czech Republic; Tsonami Arte Sonoro Festival in Valparaiso, Chile; the Huddersfield New Music Festival in the United Kingdom; and Umbral Sesiones at the Museo de Arte Contemporáneo in Oaxaca, Mexico. Recordings of my music have been released by XI Records, Another Timbre, New World Records, Edition Wandelweiser, Bahn Mi Verlag, Tsonami Records, and Pogus Productions. In 2008, I co-founded <em>the wulf.</em>, a Los Angeles-based organization dedicated to experimental performance and art that presented over 350 events in 8 years. From 2018 to 2019, I was a fellow / artist-in-residence at the Akademie Schloss Solitude in Stuttgart, Germany. I currently teach as University Professor of Sound and Intermedia at the Gustav Mahler Privatuniversität für Musik in Klagenfurt, Austria while maintaining my primary residence in Berlin, Germany.
|
||||||
My music and installations have been presented at venues and festivals throughout the world such as REDCAT, in Los Angeles; the Ostrava Festival of New Music in the Czech Republic; Tsonami Arte Sonoro Festival in Valparaiso, Chile; the Huddersfield New Music Festival in the United Kingdom; and Umbral Sesiones at the Museo de Arte Contemporáneo in Oaxaca, Mexico. Recordings of my music have been released by XI Records, Another Timbre, New World Records, Edition Wandelweiser, Bahn Mi Verlag, Tsonami Records, and Pogus Productions. In 2008, I co-founded <em>the wulf.</em>, a Los Angeles-based organization dedicated to experimental performance and art that presented over 350 events in 8 years. From 2018 to 2019, I was a fellow / artist-in-residence at the Akademie Schloss Solitude in Stuttgart, Germany. I currently teach as University Professor of Sound and Intermedia at the Gustav Mahler Privatuniversität für Musik in Klagenfurt, Austria while maintaining my primary residence in Berlin, Germany.
|
</p>
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<div id="mc_embed_signup">
|
|
||||||
<form action="https://unboundedpress.us12.list-manage.com/subscribe/post?u=bdadd25738fedf704641f3a80&id=01c5761ebb&f_id=00f143e0f0" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_self">
|
|
||||||
<label for="mce-EMAIL">subscribe to my mailing list to know about upcoming events</label>
|
|
||||||
<input id="mce-EMAIL" type="email" value="" name="EMAIL" placeholder="email address" required="" class="email">
|
|
||||||
<div style="position: absolute; left: -5000px;" aria-hidden="true">
|
|
||||||
<input type="text" name="b_bdadd25738fedf704641f3a80_01c5761ebb" tabindex="-1" value="">
|
|
||||||
</div>
|
|
||||||
<div id="mce-responses" class="clear foot">
|
|
||||||
<div class="response" id="mce-error-response" style="display:none"></div>
|
|
||||||
<div class="response" id="mce-success-response" style="display:none"></div>
|
|
||||||
</div> <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
|
|
||||||
<div class="clear">
|
|
||||||
<input id="mc-embedded-subscribe" type="submit" value="subscribe" name="subscribe" class="button">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<div class="inline-flex place-items-center p-2">
|
|
||||||
Contact
|
|
||||||
<div>
|
|
||||||
<IconButton :visible="true" type="email" work="placeholder" link="javascript:location='mailto:\u006d\u0077\u0069\u006e\u0074\u0065\u0072\u0040\u0075\u006e\u0062\u006f\u0075\u006e\u0064\u0065\u0064\u0070\u0072\u0065\u0073\u0073\u002e\u006f\u0072\u0067';void 0" class="mt-[-6px]"></IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="inline-flex place-items-center p-2">
|
|
||||||
CV
|
|
||||||
<div>
|
|
||||||
<IconButton :visible="true" type="document" work="placeholder" link="/cv" class="mt-[-6px]"></IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="inline-flex place-items-center p-2">
|
|
||||||
Works List with Presentation History
|
|
||||||
<div>
|
|
||||||
<IconButton :visible="true" type="document" work="placeholder" link="/works_list" class="mt-[-6px]"></IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="px-5">
|
|
||||||
<ImageSlider bucket="images" :gallery="gallery" class="max-w-[90%]"></ImageSlider>
|
<div class="mb-8 space-y-2">
|
||||||
|
<a href="mailto:mwinter@unboundedpress.org" class="block hover:underline">contact</a>
|
||||||
|
<a href="/cv" class="block hover:underline">cv</a>
|
||||||
|
<a href="/works_list" class="block hover:underline">works list</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="mc_embed_signup">
|
||||||
|
<form action="https://unboundedpress.us12.list-manage.com/subscribe/post?u=bdadd25738fedf704641f3a80&id=01c5761ebb&f_id=00f143e0f0" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_self">
|
||||||
|
<label for="mce-EMAIL" class="block mb-2">subscribe to my mailing list to know about upcoming events</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input id="mce-EMAIL" type="email" value="" name="EMAIL" placeholder="email address" required="" class="flex-1 px-3 py-2 border border-gray-300 rounded">
|
||||||
|
<div style="position: absolute; left: -5000px;" aria-hidden="true">
|
||||||
|
<input type="text" name="b_bdadd25738fedf704641f3a80_01c5761ebb" tabindex="-1" value="">
|
||||||
|
</div>
|
||||||
|
<input id="mc-embedded-subscribe" type="submit" value="subscribe" name="subscribe" class="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">
|
||||||
|
</div>
|
||||||
|
<div id="mce-responses" class="clear foot">
|
||||||
|
<div class="response" id="mce-error-response" style="display:none"></div>
|
||||||
|
<div class="response" id="mce-success-response" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="lg:col-span-2">
|
||||||
|
<section v-if="gallery?.length" class="lg:sticky lg:top-24">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<button
|
||||||
|
v-for="(image, index) in gallery"
|
||||||
|
:key="index"
|
||||||
|
@click="openImageModal(index)"
|
||||||
|
class="block w-full relative overflow-hidden group"
|
||||||
|
>
|
||||||
|
<nuxt-img
|
||||||
|
:src="'/images/' + image.image"
|
||||||
|
:alt="image.credit"
|
||||||
|
class="w-full aspect-[4/3] object-cover transition-opacity duration-200 group-hover:opacity-50"
|
||||||
|
/>
|
||||||
|
<p v-if="image.credit" class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 text-center text-xs px-2 bg-black/50 text-white">photo credit: {{ image.credit }}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { useModalStore } from "@/stores/ModalStore"
|
||||||
|
|
||||||
|
const modalStore = useModalStore()
|
||||||
|
|
||||||
const { data: gallery } = await useFetch('/api/my_image_gallery')
|
const { data: gallery } = await useFetch('/api/my_image_gallery')
|
||||||
|
|
||||||
|
const openImageModal = (index) => {
|
||||||
|
modalStore.setModalProps('image', 'aspect-auto', true, 'images', gallery.value, '')
|
||||||
|
}
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
titleTemplate: 'Michael Winter - About - Short Bio, Contact, CV, Works List, and Mailing List'
|
titleTemplate: 'Michael Winter - About'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
#mc_embed_signup form {text-align:left; padding:2px 0 2px 0;}
|
#mc_embed_signup input.email {
|
||||||
.mc-field-group { display: inline-block; } /* positions input field horizontally */
|
width: 100%;
|
||||||
#mc_embed_signup input.email {border: 1px solid #ABB0B2; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; color: #343434; background-color: #fff; box-sizing:border-box; height:32px; padding: 0px 0.4em; display: inline-block; margin: 0; width:350px; vertical-align:top;}
|
}
|
||||||
#mc_embed_signup label {display:block; padding-bottom:10px;}
|
@media (min-width: 640px) {
|
||||||
#mc_embed_signup .clear {display: inline-block;} /* positions button horizontally in line with input */
|
#mc_embed_signup input.email {
|
||||||
#mc_embed_signup .button {font-size: 13px; border: none; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; letter-spacing: .03em; color: #fff; background-color: #aaa; box-sizing:border-box; height:32px; line-height:32px; padding:0 18px; display: inline-block; margin: 0; transition: all 0.23s ease-in-out 0s;}
|
width: auto;
|
||||||
#mc_embed_signup .button:hover {background-color:#777; cursor:pointer;}
|
flex: 1;
|
||||||
#mc_embed_signup div#mce-responses {float:left; top:-1.4em; padding:0em .5em 0em .5em; overflow:hidden; width:90%;margin: 0 5%; clear: both;}
|
}
|
||||||
#mc_embed_signup div.response {margin:1em 0; padding:1em .5em .5em 0; font-weight:bold; float:left; top:-1.5em; z-index:1; width:80%;}
|
}
|
||||||
#mc_embed_signup #mce-error-response {display:none;}
|
#mc_embed_signup .button {
|
||||||
#mc_embed_signup #mce-success-response {color:#529214; display:none;}
|
width: auto;
|
||||||
#mc_embed_signup label.error {display:block; float:none; width:auto; margin-left:1.05em; text-align:left; padding:.5em 0;}
|
}
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 639px) {
|
||||||
#mc_embed_signup input.email {width:100%; margin-bottom:5px;}
|
#mc_embed_signup form > div {
|
||||||
#mc_embed_signup .clear {display: block; width: 100% }
|
flex-direction: column;
|
||||||
#mc_embed_signup .button {width: 100%; margin:0; }
|
}
|
||||||
}
|
#mc_embed_signup .button {
|
||||||
#mc_embed_signup{clear:left; width:100%;}
|
width: 100%;
|
||||||
</style>
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
62
pages/albums.vue
Normal file
62
pages/albums.vue
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<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({
|
navigateTo('/pieces')
|
||||||
titleTemplate: 'Michael Winter - Home / Works - Pieces, Publications, and Albums'
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
60
pages/lectures.vue
Normal file
60
pages/lectures.vue
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
74
pages/performances.vue
Normal file
74
pages/performances.vue
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
137
pages/pieces.vue
Normal file
137
pages/pieces.vue
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-5 gap-8">
|
||||||
|
<div class="lg:col-span-3">
|
||||||
|
<section>
|
||||||
|
<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 ml-4">
|
||||||
|
<div>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="hasItems(work)"
|
||||||
|
:to="'/works/' + slugify(work.title)"
|
||||||
|
class="text-lg italic hover:underline"
|
||||||
|
>
|
||||||
|
<span v-html="work.title"></span>
|
||||||
|
</NuxtLink>
|
||||||
|
<span v-else class="text-lg italic">
|
||||||
|
<span v-html="work.title"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasContent(work)" class="flex gap-4 text-sm text-gray-500 mt-0.5">
|
||||||
|
<button v-if="work.soundcloud_trackid" @click="audioPlayerStore.setSoundCloudTrackID(work.soundcloud_trackid)" class="hover:underline">
|
||||||
|
audio
|
||||||
|
</button>
|
||||||
|
<button v-if="work.vimeo_trackid" @click="openVideoModal(work.vimeo_trackid)" class="hover:underline">
|
||||||
|
video
|
||||||
|
</button>
|
||||||
|
<button v-if="work.gallery" @click="openImageModal(work)" class="hover:underline">
|
||||||
|
images
|
||||||
|
</button>
|
||||||
|
<a v-if="work.score" :href="work.score" target="_blank" class="hover:underline">
|
||||||
|
score
|
||||||
|
</a>
|
||||||
|
</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"
|
||||||
|
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
||||||
|
|
||||||
|
const modalStore = useModalStore()
|
||||||
|
const audioPlayerStore = useAudioPlayerStore()
|
||||||
|
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 hasContent = (work) => {
|
||||||
|
return work.soundcloud_trackid || work.vimeo_trackid || (work.images && work.images.length) || work.score
|
||||||
|
}
|
||||||
|
|
||||||
|
const openVideoModal = (vimeoId) => {
|
||||||
|
modalStore.setModalProps('video', 'aspect-video', true, '', '', vimeoId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openImageModal = (work) => {
|
||||||
|
const gallery = work.images.map(img => ({ image: img.filename }))
|
||||||
|
modalStore.setModalProps('image', 'aspect-auto', true, 'images', gallery, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
@ -1,29 +1,81 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-zinc-100 rounded-lg mx-5 mt-5 p-5">
|
<div>
|
||||||
<div v-if="work">
|
<div v-if="work">
|
||||||
<div class="sticky top-[100px] z-10 bg-zinc-100 pb-4 pt-2">
|
<div class="sticky top-12 bg-white z-30 border-b border-gray-200">
|
||||||
<div class="max-w-[800px] mx-auto">
|
<div class="max-w-3xl mx-auto px-4">
|
||||||
<h1 class="text-4xl italic text-center mb-4" v-html="work.title"></h1>
|
<div class="flex items-center justify-between py-1">
|
||||||
|
<div class="flex items-baseline gap-1">
|
||||||
<nav class="flex gap-4 mb-6">
|
<h1 class="text-xl italic" v-html="work.title"></h1>
|
||||||
<a v-if="work.vimeo_trackid" href="#video" class="hover:underline">Video</a>
|
<span class="text-sm text-gray-500">({{ year }})</span>
|
||||||
<a v-if="gallery && gallery.length" href="#images" class="hover:underline">Images</a>
|
</div>
|
||||||
<a v-if="scoreUrl" href="#score" class="hover:underline">Score</a>
|
<nav class="flex items-center gap-4 text-sm">
|
||||||
</nav>
|
<a
|
||||||
|
v-for="item in navItems"
|
||||||
|
:key="item.href"
|
||||||
|
:href="item.href"
|
||||||
|
class="hover:underline"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</a>
|
||||||
|
<NuxtLink to="/pieces" class="text-gray-500 hover:text-gray-700">
|
||||||
|
<Icon name="mdi:arrow-u-left-top" class="w-4 h-4 translate-y-0.5" />
|
||||||
|
</NuxtLink>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-[800px] mx-auto">
|
<div class="max-w-3xl mx-auto space-y-8 px-4 pt-6">
|
||||||
<div id="video" v-if="work.vimeo_trackid" class="mb-8 scroll-mt-[280px]">
|
<div v-if="work.soundcloud_trackid" id="audio">
|
||||||
<iframe :src="'https://player.vimeo.com/video/' + work.vimeo_trackid" width="100%" height="100%" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen class="w-full aspect-video"></iframe>
|
<h2 class="text-lg mb-4 border-b border-gray-200 pb-2">Audio</h2>
|
||||||
|
<iframe
|
||||||
|
:src="'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + work.soundcloud_trackid + '&color=%23ff5500&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true'"
|
||||||
|
width="100%"
|
||||||
|
height="150"
|
||||||
|
scrolling="no"
|
||||||
|
frameborder="no"
|
||||||
|
allow="autoplay"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="work.vimeo_trackid" id="video">
|
||||||
|
<h2 class="text-lg mb-4 border-b border-gray-200 pb-2">Video</h2>
|
||||||
|
<iframe
|
||||||
|
:src="'https://player.vimeo.com/video/' + work.vimeo_trackid"
|
||||||
|
width="100%"
|
||||||
|
height="400"
|
||||||
|
frameborder="0"
|
||||||
|
webkitallowfullscreen
|
||||||
|
mozallowfullscreen
|
||||||
|
allowfullscreen
|
||||||
|
class="w-full aspect-video"
|
||||||
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="images" v-if="gallery && gallery.length" class="mb-8 scroll-mt-[280px]">
|
<div v-if="gallery?.length" id="images">
|
||||||
<ImageSlider :bucket="'images'" :gallery="gallery"></ImageSlider>
|
<h2 class="text-lg mb-4 border-b border-gray-200 pb-2">Images</h2>
|
||||||
|
<div :class="gallery.length === 1 ? 'grid grid-cols-1' : 'grid grid-cols-2 gap-4'">
|
||||||
|
<button
|
||||||
|
v-for="(img, index) in gallery"
|
||||||
|
:key="index"
|
||||||
|
@click="openImageModal(index)"
|
||||||
|
class="block w-full relative overflow-hidden group"
|
||||||
|
>
|
||||||
|
<nuxt-img
|
||||||
|
:src="'/images/' + img.image"
|
||||||
|
:alt="work.title"
|
||||||
|
class="w-full aspect-[4/3] object-cover transition-opacity duration-200 group-hover:opacity-50"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="score" v-if="scoreUrl" class="mb-8 scroll-mt-[280px]">
|
<div v-if="scoreUrl" id="score">
|
||||||
<iframe :src="scoreUrl + '#toolbar=1&navpanes=0&sidebar=0'" class="w-full h-[85vh] border"></iframe>
|
<h2 class="text-lg mb-4 border-b border-gray-200 pb-2">Score</h2>
|
||||||
|
<iframe
|
||||||
|
:src="scoreUrl + '#toolbar=1&navpanes=0&sidebar=0'"
|
||||||
|
class="w-full h-[80vh] border border-gray-200"
|
||||||
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -34,10 +86,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
import { useModalStore } from "@/stores/ModalStore"
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const audioPlayerStore = useAudioPlayerStore()
|
const modalStore = useModalStore()
|
||||||
const slug = route.params.slug
|
const slug = route.params.slug
|
||||||
|
|
||||||
const slugify = (title) => {
|
const slugify = (title) => {
|
||||||
|
|
@ -54,6 +106,11 @@ const work = computed(() => {
|
||||||
return works.value.find(w => slugify(w.title) === slug)
|
return works.value.find(w => slugify(w.title) === slug)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const year = computed(() => {
|
||||||
|
if (!work.value?.date) return ''
|
||||||
|
return new Date(work.value.date).getFullYear()
|
||||||
|
})
|
||||||
|
|
||||||
const scoreUrl = computed(() => {
|
const scoreUrl = computed(() => {
|
||||||
if (!work.value?.score) return null
|
if (!work.value?.score) return null
|
||||||
if (work.value.score.startsWith('/scores/')) {
|
if (work.value.score.startsWith('/scores/')) {
|
||||||
|
|
@ -69,23 +126,27 @@ const gallery = computed(() => {
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
const itemCount = computed(() => {
|
const navItems = computed(() => {
|
||||||
let count = 0
|
const items = []
|
||||||
if (work.value?.vimeo_trackid) count++
|
if (work.value?.soundcloud_trackid) items.push({ label: 'Audio', href: '#audio' })
|
||||||
if (gallery.value?.length) count++
|
if (work.value?.vimeo_trackid) items.push({ label: 'Video', href: '#video' })
|
||||||
if (scoreUrl.value) count++
|
if (gallery.value?.length) items.push({ label: 'Images', href: '#images' })
|
||||||
return count
|
if (scoreUrl.value) items.push({ label: 'Score', href: '#score' })
|
||||||
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
const openImageModal = (index) => {
|
||||||
if (work.value?.soundcloud_trackid) {
|
modalStore.setModalProps('image', 'aspect-auto', true, 'images', gallery.value, '')
|
||||||
audioPlayerStore.setSoundCloudTrackID(work.value.soundcloud_trackid)
|
}
|
||||||
}
|
|
||||||
|
useHead({
|
||||||
|
titleTemplate: () => work.value ? `Michael Winter - ${work.value.title}` : 'Michael Winter'
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
scroll-padding-top: 100px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
64
pages/writings.vue
Normal file
64
pages/writings.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<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>
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
|
|
||||||
export const useAudioPlayerStore = defineStore("AudioPlayerStore", {
|
export const useAudioPlayerStore = defineStore("AudioPlayerStore", {
|
||||||
state: () => ({"soundcloud_trackid": "1032587794"}),
|
state: () => ({
|
||||||
|
"soundcloud_trackid": "undefined",
|
||||||
|
"currentTrackTitle": ""
|
||||||
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setSoundCloudTrackID(trackid) {
|
setSoundCloudTrackID(trackid, title = "") {
|
||||||
if (typeof trackid !== 'undefined') {
|
if (typeof trackid !== 'undefined') {
|
||||||
this.soundcloud_trackid = trackid
|
this.soundcloud_trackid = trackid
|
||||||
|
this.currentTrackTitle = title
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSoundCloudTrackID() {
|
clearSoundCloudTrackID() {
|
||||||
this.soundcloud_trackid = 'undefined'
|
this.soundcloud_trackid = 'undefined'
|
||||||
|
this.currentTrackTitle = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue