Compare commits
3 commits
2fe7dad9fa
...
6ffe5aa1fc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ffe5aa1fc | ||
|
|
635cbe0482 | ||
|
|
9af20c1e5d |
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="inline-flex p-1 min-w-[25px]">
|
<div class="inline-flex p-1 min-w-[25px]">
|
||||||
<div v-show="visible" class="bg-black rounded-full text-xs inline-flex" >
|
<div v-show="visible" class="bg-black rounded-full text-xs inline-flex" >
|
||||||
|
|
||||||
<button v-if="type === 'score'" @click="modalStore.setModalProps('pdf', 'aspect-[1/1.414]', true, '', '', '', link, work.soundcloud_trackid ? 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + work.soundcloud_trackid + '&auto_play=true&show_user=false' : '')" class="inline-flex p-1">
|
<button v-if="type === 'score'" @click="openWithHash('score')" class="inline-flex p-1">
|
||||||
<Icon name="ion:book-sharp" style="color: white" />
|
<Icon name="ion:book-sharp" style="color: white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
@ -26,11 +26,11 @@
|
||||||
<Icon name="wpf:speaker" style="color: white" />
|
<Icon name="wpf:speaker" style="color: white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="modalStore.setModalProps('video', 'aspect-video', true, '', '', work.vimeo_trackid)" v-else-if="type === 'video'" class="inline-flex p-1">
|
<button @click="openWithHash('video')" v-else-if="type === 'video'" class="inline-flex p-1">
|
||||||
<Icon name="fluent:video-48-filled" style="color: white" />
|
<Icon name="fluent:video-48-filled" style="color: white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button @click="modalStore.setModalProps('image', 'aspect-auto', true, 'images', work.gallery, '', '', work.soundcloud_trackid ? 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + work.soundcloud_trackid + '&auto_play=true&show_user=false' : '')" v-else="type === 'image'" class="inline-flex p-1">
|
<button @click="openWithHash('image')" v-else-if="type === 'image'" class="inline-flex p-1">
|
||||||
<Icon name="mdi:camera" style="color: white" />
|
<Icon name="mdi:camera" style="color: white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
@ -48,6 +48,13 @@
|
||||||
const audioPlayerStore = useAudioPlayerStore()
|
const audioPlayerStore = useAudioPlayerStore()
|
||||||
const modalStore = useModalStore()
|
const modalStore = useModalStore()
|
||||||
|
|
||||||
|
const slugify = (title) => {
|
||||||
|
if (!title) return ''
|
||||||
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const workSlug = computed(() => slugify(props.work?.title))
|
||||||
|
|
||||||
const isExternalLink = computed(() => {
|
const isExternalLink = computed(() => {
|
||||||
return props.link && !props.link.endsWith('.pdf') && !props.link.startsWith('/')
|
return props.link && !props.link.endsWith('.pdf') && !props.link.startsWith('/')
|
||||||
})
|
})
|
||||||
|
|
@ -63,4 +70,21 @@
|
||||||
modalStore.setModalProps('document', 'aspect-[1/1.414]', true, '', '', '', '', '', props.link)
|
modalStore.setModalProps('document', 'aspect-[1/1.414]', true, '', '', '', '', '', props.link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openWithHash = (type) => {
|
||||||
|
const slug = workSlug.value
|
||||||
|
const hash = type + '|' + slug
|
||||||
|
|
||||||
|
if (type === 'score') {
|
||||||
|
modalStore.setModalProps('pdf', 'aspect-[1/1.414]', true, '', '', '', props.link, props.work?.soundcloud_trackid ? 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + props.work.soundcloud_trackid + '&auto_play=true&show_user=false' : '')
|
||||||
|
} else if (type === 'video') {
|
||||||
|
modalStore.setModalProps('video', 'aspect-video', true, '', '', props.work.vimeo_trackid)
|
||||||
|
} else if (type === 'image') {
|
||||||
|
modalStore.setModalProps('image', 'aspect-auto', true, 'images', props.work.gallery, '', '', props.work.soundcloud_trackid ? 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + props.work.soundcloud_trackid + '&auto_play=true&show_user=false' : '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slug && typeof window !== 'undefined') {
|
||||||
|
window.history.replaceState({}, '', '/#' + hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
258
components/IndexContent.vue
Normal file
258
components/IndexContent.vue
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
<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="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="'/work/' + slugify(work.title)" class="italic text-sm">
|
||||||
|
<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>
|
||||||
|
<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]">
|
||||||
|
{{ 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>
|
||||||
|
<IconButton :visible=item.entryTags.howpublished type="document" :link=item.entryTags.howpublished class="inline-flex p-1 mt-[-6px]"></IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useModalStore } from "@/stores/ModalStore"
|
||||||
|
import { watchEffect, onMounted } from "vue"
|
||||||
|
|
||||||
|
const props = defineProps(['slug'])
|
||||||
|
|
||||||
|
const modalStore = useModalStore()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const slugify = (title) => {
|
||||||
|
if (!title) return ''
|
||||||
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});
|
||||||
|
|
||||||
|
const hasItems = (work) => {
|
||||||
|
return work.vimeo_trackid || (work.images && work.images.length) || work.score
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidUrl = urlString => {
|
||||||
|
var pattern = /^((http|https|ftp):\/\/)/;
|
||||||
|
return pattern.test(urlString)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: works } = await useFetch('/api/works', {
|
||||||
|
key: 'works-indexcontent',
|
||||||
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: pubs } = await useFetch('/api/publications', {
|
||||||
|
key: 'publications-indexcontent',
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: releases } = await useFetch('/api/releases', {
|
||||||
|
key: 'releases-indexcontent',
|
||||||
|
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 findWorkBySlug = (slugToFind) => {
|
||||||
|
if (!works.value) return null
|
||||||
|
for (const group of works.value) {
|
||||||
|
const found = group.works.find(w => slugify(w.title) === slugToFind)
|
||||||
|
if (found) return found
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const openAlbumModal = (album) => {
|
||||||
|
modalStore.setModalProps('image', 'aspect-auto', true, 'album_art', [{image: album.album_art}], '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModalFromHash = () => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
|
||||||
|
const hash = window.location.hash.slice(1)
|
||||||
|
if (!hash) return
|
||||||
|
|
||||||
|
const [hashType, hashSlug] = hash.includes('|') ? hash.split('|') : [null, null]
|
||||||
|
if (!hashType || !hashSlug) return
|
||||||
|
|
||||||
|
const work = findWorkBySlug(hashSlug)
|
||||||
|
if (!work) return
|
||||||
|
|
||||||
|
if (hashType === 'score' && work.score) {
|
||||||
|
modalStore.setModalProps(
|
||||||
|
'pdf',
|
||||||
|
'aspect-[1/1.414]',
|
||||||
|
true,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
work.score,
|
||||||
|
work.soundcloud_trackid ? 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + work.soundcloud_trackid + '&auto_play=true&show_user=false' : ''
|
||||||
|
)
|
||||||
|
} else if (hashType === 'video' && work.vimeo_trackid) {
|
||||||
|
modalStore.setModalProps('video', 'aspect-video', true, '', '', work.vimeo_trackid)
|
||||||
|
} else if (hashType === 'image' && work.gallery) {
|
||||||
|
modalStore.setModalProps(
|
||||||
|
'image',
|
||||||
|
'aspect-auto',
|
||||||
|
true,
|
||||||
|
'images',
|
||||||
|
work.gallery,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
work.soundcloud_trackid ? 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + work.soundcloud_trackid + '&auto_play=true&show_user=false' : ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.slug && works.value) {
|
||||||
|
const work = findWorkBySlug(props.slug)
|
||||||
|
if (work?.score) {
|
||||||
|
modalStore.setModalProps(
|
||||||
|
'pdf',
|
||||||
|
'aspect-[1/1.414]',
|
||||||
|
true,
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
work.score,
|
||||||
|
work.soundcloud_trackid ? 'https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/' + work.soundcloud_trackid + '&auto_play=true&show_user=false' : ''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.slug && typeof window !== 'undefined') {
|
||||||
|
openModalFromHash()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => modalStore.isOpen, (isOpen) => {
|
||||||
|
if (!isOpen && typeof window !== 'undefined' && window.location.hash) {
|
||||||
|
window.history.replaceState({}, '', window.location.pathname)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
160
pages/index.vue
160
pages/index.vue
|
|
@ -1,167 +1,9 @@
|
||||||
<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">
|
<IndexContent />
|
||||||
|
|
||||||
<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">
|
|
||||||
<span v-html="work.title" class="italic text-sm"></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>
|
|
||||||
<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]">
|
|
||||||
{{ 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>
|
|
||||||
<IconButton :visible=item.entryTags.howpublished type="document" :link=item.entryTags.howpublished class="inline-flex p-1 mt-[-6px]"></IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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="modalStore.setModalProps('image', 'aspect-auto', true, 'album_art', [{image: item.album_art}], '')">
|
|
||||||
<nuxt-img :src="'/album_art/' + item.album_art"
|
|
||||||
quality="50"/>
|
|
||||||
</button>
|
|
||||||
<div class="flex place-content-center place-items-center">
|
|
||||||
<IconButton :visible="item.discogs_id" type="discogs" :link="'https://www.discogs.com/release/' + item.discogs_id" :newTab="true"></IconButton>
|
|
||||||
<IconButton :visible="item.buy_link" type="buy" :link="item.buy_link" :newTab="true"></IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
|
||||||
import { useModalStore } from "@/stores/ModalStore"
|
|
||||||
|
|
||||||
const modalStore = useModalStore()
|
|
||||||
|
|
||||||
const groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});
|
|
||||||
|
|
||||||
const isValidUrl = urlString => {
|
|
||||||
var pattern = /^((http|https|ftp):\/\/)/;
|
|
||||||
return pattern.test(urlString)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const { data: images } = await useFetch('/api/images', { key: 'images' })
|
|
||||||
|
|
||||||
const { data: works } = await useFetch('/api/works', {
|
|
||||||
key: 'works',
|
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: pubs } = await useFetch('/api/publications', {
|
|
||||||
key: 'publications',
|
|
||||||
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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: releases } = await useFetch('/api/releases', {
|
|
||||||
key: 'releases',
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
titleTemplate: 'Michael Winter - Home / Works - Pieces, Publications, and Albums'
|
titleTemplate: 'Michael Winter - Home / Works - Pieces, Publications, and Albums'
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
91
pages/work/[slug].vue
Normal file
91
pages/work/[slug].vue
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<template>
|
||||||
|
<div class="bg-zinc-100 rounded-lg mx-5 mt-5 p-5">
|
||||||
|
<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 v-if="itemCount >= 2" class="bg-zinc-200 flex gap-2 p-2 rounded-lg shadow border border-zinc-300 mb-6">
|
||||||
|
<a v-if="work.vimeo_trackid" href="#video" class="px-4 py-2 bg-white rounded hover:bg-gray-200 transition font-medium">Video</a>
|
||||||
|
<a v-if="gallery && gallery.length" href="#images" class="px-4 py-2 bg-white rounded hover:bg-gray-200 transition font-medium">Images</a>
|
||||||
|
<a v-if="scoreUrl" href="#score" class="px-4 py-2 bg-white rounded hover:bg-gray-200 transition font-medium">Score</a>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center p-10">
|
||||||
|
<p>Work not found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useAudioPlayerStore } from "@/stores/AudioPlayerStore"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const audioPlayerStore = useAudioPlayerStore()
|
||||||
|
const slug = route.params.slug
|
||||||
|
|
||||||
|
const slugify = (title) => {
|
||||||
|
if (!title) return ''
|
||||||
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: works } = await useFetch('/api/works', {
|
||||||
|
key: 'works-workpage-' + slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
const work = computed(() => {
|
||||||
|
if (!works.value) return null
|
||||||
|
return works.value.find(w => slugify(w.title) === slug)
|
||||||
|
})
|
||||||
|
|
||||||
|
const scoreUrl = computed(() => {
|
||||||
|
if (!work.value?.score) return null
|
||||||
|
if (work.value.score.startsWith('/scores/')) {
|
||||||
|
return work.value.score
|
||||||
|
}
|
||||||
|
return '/scores/' + work.value.score
|
||||||
|
})
|
||||||
|
|
||||||
|
const gallery = computed(() => {
|
||||||
|
if (!work.value?.images) return null
|
||||||
|
return work.value.images.map(img => ({
|
||||||
|
image: img.filename
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const itemCount = computed(() => {
|
||||||
|
let count = 0
|
||||||
|
if (work.value?.vimeo_trackid) count++
|
||||||
|
if (gallery.value?.length) count++
|
||||||
|
if (scoreUrl.value) count++
|
||||||
|
return count
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (work.value?.soundcloud_trackid) {
|
||||||
|
audioPlayerStore.setSoundCloudTrackID(work.value.soundcloud_trackid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
server/routes/scores/[...path].ts
Normal file
25
server/routes/scores/[...path].ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { existsSync, createReadStream } from 'fs'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { sendStream } from 'h3'
|
||||||
|
import { createError } from 'h3'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const url = event.path
|
||||||
|
|
||||||
|
// Get the filename from the URL
|
||||||
|
const filename = url.replace('/scores/', '')
|
||||||
|
|
||||||
|
// Check if file exists in public/scores/
|
||||||
|
const filePath = join(process.cwd(), 'public/scores', filename)
|
||||||
|
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
statusMessage: 'Not Found'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve the file
|
||||||
|
event.node.res.statusCode = 200
|
||||||
|
return sendStream(event, createReadStream(filePath))
|
||||||
|
})
|
||||||
Loading…
Reference in a new issue