Compare commits

...

10 commits

14 changed files with 856 additions and 230 deletions

View file

@ -1,5 +1,5 @@
<template>
<div class="bg-white min-w-[800px] min-h-[80vh]">
<div class="bg-white min-h-[80vh]">
<NuxtLayout>
<NuxtPage/>
</NuxtLayout>

View file

@ -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 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">
<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 text-sm">
<span v-else class="italic">
<span v-html="work.title"></span>
</span>
<div class="inline-flex mt-[-4px]">
<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>
<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>
<IconButton :visible="work.score" type="score" :work="work" :link="work.score" class="inline-flex p-1"></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>
</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]">
<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}}.&nbsp;</span>
<span v-if=item.entryTags.journal>{{item.entryTags.journal}}.&nbsp;</span>
<span v-if=item.entryTags.editor>editors {{item.entryTags.editor}}&nbsp;</span>
<span v-if=item.entryTags.volume>volume {{item.entryTags.volume}}.</span>
<span v-if=item.entryTags.publisher>{{item.entryTags.publisher}}.</span>
<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>
<IconButton :visible=item.entryTags.howpublished type="document" :link=item.entryTags.howpublished class="inline-flex p-1 mt-[-6px]"></IconButton>
</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">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"/>
<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 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 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>
</section>
</div>
</div>
</template>

112
components/Menu.vue Normal file
View 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>

View file

@ -1,45 +1,78 @@
<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">
<!-- Mobile header -->
<header class="sticky top-0 bg-white z-40 border-b border-gray-200 lg:hidden">
<div class="max-w-7xl mx-auto px-4 py-2 flex items-center justify-between gap-4">
<Menu />
<h1 class="text-lg md:text-2xl whitespace-nowrap">
<NuxtLink to='/' class="hover:underline">michael winter</NuxtLink>
</h1>
<div class="flex-1 text-right text-lg md:text-xl">{{ pageTitle }}</div>
</div>
</header>
<!-- Desktop header -->
<header class="sticky top-0 bg-white z-40 border-b border-gray-200 hidden lg:block">
<div class="w-full px-4 py-2 flex items-center justify-between">
<h1 class="text-lg md:text-2xl whitespace-nowrap">
<NuxtLink to='/' class="hover:underline">michael winter</NuxtLink>
</h1>
<div class="text-lg md:text-xl">{{ pageTitle }}</div>
</div>
</header>
<div class="flex">
<!-- Sidebar for wide screens -->
<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">
<nav class="space-y-4 ml-4">
<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>
<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">pieces</NuxtLink>
<NuxtLink to="/writings" class="block hover:underline">writings</NuxtLink>
<NuxtLink to="/albums" class="block hover:underline">albums</NuxtLink>
</div>
</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>
<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>
<!-- 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>
-->
<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>
<slot /> <!-- required here only -->
<div class="fixed bottom-0 bg-white p-2 w-full flex justify-center z-20">
</div>
<footer class="fixed bottom-0 bg-white border-t border-gray-200 p-2 w-full flex justify-center z-30">
<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>
<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>
</div>
</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">
<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>
</div>
<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>
<div v-if="modalStore.soundcloudUrl" class="flex justify-center mt-2">
<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>
</div>
</div>
@ -69,18 +102,31 @@
</div>
</ModalBody>
</Modal>
</div>
</template>
<script setup>
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
import { useModalStore } from "@/stores/ModalStore"
import { onMounted } from "vue"
import { onMounted, computed } from "vue"
const audioPlayerStore = useAudioPlayerStore()
const modalStore = useModalStore()
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(() => {
if(route.params.files == 'scores') {
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>

View file

@ -19,6 +19,7 @@ export default defineNuxtConfig({
},
},
routeRules: {
'/': { redirect: '/pieces' },
'/hdp': { redirect: '/a_history_of_the_domino_problem' },
// Default: prerender all routes (static HTML)

View file

@ -1,91 +1,98 @@
<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="px-5">
<p class="text-lg">about</p>
<div class="leading-tight py-2 ml-3 text-sm">
<div class="leading-tight py-2">
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
<div class="lg:col-span-3">
<section>
<div class="mb-8">
<p class="mb-4">
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.
</div>
<div class="leading-tight py-2">
</p>
<p>
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 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 id="mc_embed_signup">
<form action="https://unboundedpress.us12.list-manage.com/subscribe/post?u=bdadd25738fedf704641f3a80&amp;id=01c5761ebb&amp;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">
<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> <!-- 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>
</section>
</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>
<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 class="px-5">
<ImageSlider bucket="images" :gallery="gallery" class="max-w-[90%]"></ImageSlider>
</section>
</div>
</div>
</template>
<script setup>
import { useModalStore } from "@/stores/ModalStore"
const modalStore = useModalStore()
const { data: gallery } = await useFetch('/api/my_image_gallery')
const openImageModal = (index) => {
modalStore.setModalProps('image', 'aspect-auto', true, 'images', gallery.value, '')
}
useHead({
titleTemplate: 'Michael Winter - About - Short Bio, Contact, CV, Works List, and Mailing List'
})
titleTemplate: 'Michael Winter - About'
})
</script>
<style>
#mc_embed_signup form {text-align:left; padding:2px 0 2px 0;}
.mc-field-group { display: inline-block; } /* positions input field horizontally */
#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;}
#mc_embed_signup .clear {display: inline-block;} /* positions button horizontally in line with input */
#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;}
#mc_embed_signup .button:hover {background-color:#777; cursor:pointer;}
#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 #mce-success-response {color:#529214; display:none;}
#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) {
#mc_embed_signup input.email {width:100%; margin-bottom:5px;}
#mc_embed_signup .clear {display: block; width: 100% }
#mc_embed_signup .button {width: 100%; margin:0; }
<style scoped>
#mc_embed_signup input.email {
width: 100%;
}
@media (min-width: 640px) {
#mc_embed_signup input.email {
width: auto;
flex: 1;
}
#mc_embed_signup{clear:left; width:100%;}
}
#mc_embed_signup .button {
width: auto;
}
@media (max-width: 639px) {
#mc_embed_signup form > div {
flex-direction: column;
}
#mc_embed_signup .button {
width: 100%;
}
}
</style>

62
pages/albums.vue Normal file
View 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>

View file

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

60
pages/lectures.vue Normal file
View 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
View 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
View 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>

View file

@ -1,29 +1,81 @@
<template>
<div class="bg-zinc-100 rounded-lg mx-5 mt-5 p-5">
<div>
<div v-if="work">
<div class="sticky top-[100px] z-10 bg-zinc-100 pb-4 pt-2">
<div class="max-w-[800px] mx-auto">
<h1 class="text-4xl italic text-center mb-4" v-html="work.title"></h1>
<nav class="flex gap-4 mb-6">
<a v-if="work.vimeo_trackid" href="#video" class="hover:underline">Video</a>
<a v-if="gallery && gallery.length" href="#images" class="hover:underline">Images</a>
<a v-if="scoreUrl" href="#score" class="hover:underline">Score</a>
<div class="sticky top-12 bg-white z-30 border-b border-gray-200">
<div class="max-w-3xl mx-auto px-4">
<div class="flex items-center justify-between py-1">
<div class="flex items-baseline gap-1">
<h1 class="text-xl italic" v-html="work.title"></h1>
<span class="text-sm text-gray-500">({{ year }})</span>
</div>
<nav class="flex items-center gap-4 text-sm">
<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 class="max-w-[800px] mx-auto">
<div id="video" v-if="work.vimeo_trackid" class="mb-8 scroll-mt-[280px]">
<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>
</div>
<div id="images" v-if="gallery && gallery.length" class="mb-8 scroll-mt-[280px]">
<ImageSlider :bucket="'images'" :gallery="gallery"></ImageSlider>
<div class="max-w-3xl mx-auto space-y-8 px-4 pt-6">
<div v-if="work.soundcloud_trackid" id="audio">
<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 id="score" v-if="scoreUrl" class="mb-8 scroll-mt-[280px]">
<iframe :src="scoreUrl + '#toolbar=1&navpanes=0&sidebar=0'" class="w-full h-[85vh] border"></iframe>
<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 v-if="gallery?.length" id="images">
<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 v-if="scoreUrl" id="score">
<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>
@ -34,10 +86,10 @@
</template>
<script setup>
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
import { useModalStore } from "@/stores/ModalStore"
const route = useRoute()
const audioPlayerStore = useAudioPlayerStore()
const modalStore = useModalStore()
const slug = route.params.slug
const slugify = (title) => {
@ -54,6 +106,11 @@ const work = computed(() => {
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(() => {
if (!work.value?.score) return null
if (work.value.score.startsWith('/scores/')) {
@ -69,23 +126,27 @@ const gallery = computed(() => {
}))
})
const itemCount = computed(() => {
let count = 0
if (work.value?.vimeo_trackid) count++
if (gallery.value?.length) count++
if (scoreUrl.value) count++
return count
const navItems = computed(() => {
const items = []
if (work.value?.soundcloud_trackid) items.push({ label: 'Audio', href: '#audio' })
if (work.value?.vimeo_trackid) items.push({ label: 'Video', href: '#video' })
if (gallery.value?.length) items.push({ label: 'Images', href: '#images' })
if (scoreUrl.value) items.push({ label: 'Score', href: '#score' })
return items
})
onMounted(() => {
if (work.value?.soundcloud_trackid) {
audioPlayerStore.setSoundCloudTrackID(work.value.soundcloud_trackid)
}
const openImageModal = (index) => {
modalStore.setModalProps('image', 'aspect-auto', true, 'images', gallery.value, '')
}
useHead({
titleTemplate: () => work.value ? `Michael Winter - ${work.value.title}` : 'Michael Winter'
})
</script>
<style>
html {
scroll-behavior: smooth;
scroll-padding-top: 100px;
}
</style>

64
pages/writings.vue Normal file
View 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>

View file

@ -1,15 +1,20 @@
import {defineStore} from "pinia";
export const useAudioPlayerStore = defineStore("AudioPlayerStore", {
state: () => ({"soundcloud_trackid": "1032587794"}),
state: () => ({
"soundcloud_trackid": "undefined",
"currentTrackTitle": ""
}),
actions: {
setSoundCloudTrackID(trackid) {
setSoundCloudTrackID(trackid, title = "") {
if (typeof trackid !== 'undefined') {
this.soundcloud_trackid = trackid
this.currentTrackTitle = title
}
},
clearSoundCloudTrackID() {
this.soundcloud_trackid = 'undefined'
this.currentTrackTitle = ""
}
}
})