2026-03-07 11:28:55 +01:00
|
|
|
<template>
|
|
|
|
|
<div class="grid grid-cols-1 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>
|
2026-03-07 13:34:02 +01:00
|
|
|
<div v-for="work in item.works" :key="work.title" class="mb-4 ml-4">
|
|
|
|
|
<div>
|
2026-03-07 11:28:55 +01:00
|
|
|
<NuxtLink
|
|
|
|
|
v-if="hasItems(work)"
|
|
|
|
|
:to="'/works/' + slugify(work.title)"
|
2026-03-07 13:34:02 +01:00
|
|
|
class="text-lg italic hover:underline"
|
2026-03-07 11:28:55 +01:00
|
|
|
>
|
|
|
|
|
<span v-html="work.title"></span>
|
|
|
|
|
</NuxtLink>
|
2026-03-07 13:34:02 +01:00
|
|
|
<span v-else class="text-lg italic">
|
2026-03-07 11:28:55 +01:00
|
|
|
<span v-html="work.title"></span>
|
|
|
|
|
</span>
|
2026-03-07 13:34:02 +01:00
|
|
|
</div>
|
2026-03-07 11:28:55 +01:00
|
|
|
|
2026-03-07 13:34:02 +01:00
|
|
|
<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>
|
2026-03-07 11:28:55 +01:00
|
|
|
</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"
|
2026-03-07 13:34:02 +01:00
|
|
|
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
2026-03-07 11:28:55 +01:00
|
|
|
|
|
|
|
|
const modalStore = useModalStore()
|
2026-03-07 13:34:02 +01:00
|
|
|
const audioPlayerStore = useAudioPlayerStore()
|
2026-03-07 11:28:55 +01:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 13:34:02 +01:00
|
|
|
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, '')
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 11:28:55 +01:00
|
|
|
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>
|