Add shareable URLs for works with scores

- Score icon updates URL to /?work=[slug] without navigation
- Modal closes to reset URL back to /
- Direct /work/[slug] access redirects to /?work=[slug]
- Index page opens modal when ?work= query param is present
- Works with SSG/prerendering for /?work= routes
This commit is contained in:
Michael Winter 2026-03-06 09:19:25 +01:00
parent 2fe7dad9fa
commit 9af20c1e5d
3 changed files with 77 additions and 2 deletions

View file

@ -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="openScoreModal()" class="inline-flex p-1">
<Icon name="ion:book-sharp" style="color: white" /> <Icon name="ion:book-sharp" style="color: white" />
</button> </button>
@ -41,13 +41,20 @@
<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 { computed } from "vue" import { computed, watch } from "vue"
const props = defineProps(['type', 'work', 'visible', 'link', 'newTab']) const props = defineProps(['type', 'work', 'visible', 'link', 'newTab'])
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,17 @@
modalStore.setModalProps('document', 'aspect-[1/1.414]', true, '', '', '', '', '', props.link) modalStore.setModalProps('document', 'aspect-[1/1.414]', true, '', '', '', '', '', props.link)
} }
} }
const openScoreModal = () => {
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' : '')
if (workSlug.value && typeof window !== 'undefined') {
window.history.replaceState({}, '', '/?work=' + workSlug.value)
}
}
watch(() => modalStore.isOpen, (isOpen) => {
if (!isOpen && workSlug.value && typeof window !== 'undefined' && window.location.search.includes('work=')) {
window.history.replaceState({}, '', '/')
}
})
</script> </script>

View file

@ -78,8 +78,16 @@
<script setup> <script setup>
import { useModalStore } from "@/stores/ModalStore" import { useModalStore } from "@/stores/ModalStore"
import { watch, watchEffect } from "vue"
const modalStore = useModalStore() const modalStore = useModalStore()
const route = useRoute()
const router = useRouter()
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 groupBy = (x,f)=>x.reduce((a,b,i)=>((a[f(b,i,x)]||=[]).push(b),a),{});
@ -164,4 +172,37 @@
titleTemplate: 'Michael Winter - Home / Works - Pieces, Publications, and Albums' titleTemplate: 'Michael Winter - Home / Works - Pieces, Publications, and Albums'
}) })
const workFromSlug = computed(() => {
const workSlug = route.query.work
if (!workSlug || !works.value) return null
for (const group of works.value) {
const found = group.works.find(w => slugify(w.title) === workSlug)
if (found) return found
}
return null
})
watchEffect(() => {
const workSlug = route.query.work
const work = workFromSlug.value
if (workSlug && 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' : ''
)
}
})
watch(() => modalStore.isOpen, (isOpen) => {
if (!isOpen && route.query.work && typeof window !== 'undefined') {
window.history.replaceState({}, '', '/')
}
})
</script> </script>

14
pages/work/[slug].vue Normal file
View file

@ -0,0 +1,14 @@
<script setup>
const route = useRoute()
const router = useRouter()
const slug = route.params.slug
onMounted(() => {
router.replace('/?work=' + slug)
})
</script>
<template>
<div></div>
</template>