Remove unused Modal, Collapsible components and events page

This commit is contained in:
Michael Winter 2026-03-08 18:27:08 +01:00
parent e5e4dca8e4
commit 44c3397d85
11 changed files with 0 additions and 800 deletions

View file

@ -1,313 +0,0 @@
<script>
export default {
name: 'CollapseTransition',
props: {
name: {
type: String,
required: false,
default: 'collapse',
},
dimension: {
type: String,
required: false,
default: 'height',
validator: (value) => {
return ['height', 'width'].includes(value)
},
},
duration: {
type: Number,
required: false,
default: 300,
},
easing: {
type: String,
required: false,
default: 'ease-in-out',
},
},
emits: ['before-appear', 'appear', 'after-appear', 'appear-cancelled', 'before-enter', 'enter', 'after-enter', 'enter-cancelled', 'before-leave', 'leave', 'after-leave', 'leave-cancelled'],
data() {
return {
cachedStyles: null,
}
},
computed: {
transition() {
const transitions = []
Object.keys(this.cachedStyles).forEach((key) => {
transitions.push(
`${this.convertToCssProperty(key)} ${this.duration}ms ${this.easing}`,
)
})
return transitions.join(', ')
},
},
watch: {
dimension() {
this.clearCachedDimensions()
},
},
methods: {
beforeAppear(el) {
// Emit the event to the parent
this.$emit('before-appear', el)
},
appear(el) {
// Emit the event to the parent
this.$emit('appear', el)
},
afterAppear(el) {
// Emit the event to the parent
this.$emit('after-appear', el)
},
appearCancelled(el) {
// Emit the event to the parent
this.$emit('appear-cancelled', el)
},
beforeEnter(el) {
// Emit the event to the parent
this.$emit('before-enter', el)
},
enter(el, done) {
// Because width and height may be 'auto',
// first detect and cache the dimensions
this.detectAndCacheDimensions(el)
// The order of applying styles is important:
// - 1. Set styles for state before transition
// - 2. Force repaint
// - 3. Add transition style
// - 4. Set styles for state after transition
// If the order is not right and you open any 2nd level submenu
// for the first time, the transition will not work.
this.setClosedDimensions(el)
this.hideOverflow(el)
this.forceRepaint(el)
this.setTransition(el)
this.setOpenedDimensions(el)
// Emit the event to the parent
this.$emit('enter', el, done)
// Call done() when the transition ends
// to trigger the @after-enter event.
setTimeout(done, this.duration)
},
afterEnter(el) {
// Clean up inline styles
this.unsetOverflow(el)
this.unsetTransition(el)
this.unsetDimensions(el)
this.clearCachedDimensions()
// Emit the event to the parent
this.$emit('after-enter', el)
},
enterCancelled(el) {
// Emit the event to the parent
this.$emit('enter-cancelled', el)
},
beforeLeave(el) {
// Emit the event to the parent
this.$emit('before-leave', el)
},
leave(el, done) {
// For some reason, @leave triggered when starting
// from open state on page load. So for safety,
// check if the dimensions have been cached.
this.detectAndCacheDimensions(el)
// The order of applying styles is less important
// than in the enter phase, as long as we repaint
// before setting the closed dimensions.
// But it is probably best to use the same
// order as the enter phase.
this.setOpenedDimensions(el)
this.hideOverflow(el)
this.forceRepaint(el)
this.setTransition(el)
this.setClosedDimensions(el)
// Emit the event to the parent
this.$emit('leave', el, done)
// Call done() when the transition ends
// to trigger the @after-leave event.
// This will also cause v-show
// to reapply 'display: none'.
setTimeout(done, this.duration)
},
afterLeave(el) {
// Clean up inline styles
this.unsetOverflow(el)
this.unsetTransition(el)
this.unsetDimensions(el)
this.clearCachedDimensions()
// Emit the event to the parent
this.$emit('after-leave', el)
},
leaveCancelled(el) {
// Emit the event to the parent
this.$emit('leave-cancelled', el)
},
detectAndCacheDimensions(el) {
// Cache actual dimensions
// only once to void invalid values when
// triggering during a transition
if (this.cachedStyles)
return
const visibility = el.style.visibility
const display = el.style.display
// Trick to get the width and
// height of a hidden element
el.style.visibility = 'hidden'
el.style.display = ''
this.cachedStyles = this.detectRelevantDimensions(el)
// Restore any original styling
el.style.visibility = visibility
el.style.display = display
},
clearCachedDimensions() {
this.cachedStyles = null
},
detectRelevantDimensions(el) {
// These properties will be transitioned
if (this.dimension === 'height') {
return {
height: `${el.offsetHeight}px`,
paddingTop:
el.style.paddingTop || this.getCssValue(el, 'padding-top'),
paddingBottom:
el.style.paddingBottom || this.getCssValue(el, 'padding-bottom'),
}
}
if (this.dimension === 'width') {
return {
width: `${el.offsetWidth}px`,
paddingLeft:
el.style.paddingLeft || this.getCssValue(el, 'padding-left'),
paddingRight:
el.style.paddingRight || this.getCssValue(el, 'padding-right'),
}
}
return {}
},
setTransition(el) {
el.style.transition = this.transition
},
unsetTransition(el) {
el.style.transition = ''
},
hideOverflow(el) {
el.style.overflow = 'hidden'
},
unsetOverflow(el) {
el.style.overflow = ''
},
setClosedDimensions(el) {
Object.keys(this.cachedStyles).forEach((key) => {
el.style[key] = '0'
})
},
setOpenedDimensions(el) {
Object.keys(this.cachedStyles).forEach((key) => {
el.style[key] = this.cachedStyles[key]
})
},
unsetDimensions(el) {
Object.keys(this.cachedStyles).forEach((key) => {
el.style[key] = ''
})
},
forceRepaint(el) {
// Force repaint to make sure the animation is triggered correctly.
// Thanks: https://markus.oberlehner.net/blog/transition-to-height-auto-with-vue/
// eslint-disable-next-line no-unused-expressions
getComputedStyle(el)[this.dimension]
},
getCssValue(el, style) {
return getComputedStyle(el, null).getPropertyValue(style)
},
convertToCssProperty(style) {
// Example: convert 'paddingTop' to 'padding-top'
// Thanks: https://gist.github.com/tan-yuki/3450323
const upperChars = style.match(/([A-Z])/g)
if (!upperChars)
return style
for (let i = 0, n = upperChars.length; i < n; i++) {
style = style.replace(
new RegExp(upperChars[i]),
`-${upperChars[i].toLowerCase()}`,
)
}
if (style.slice(0, 1) === '-')
style = style.slice(1)
return style
},
},
}
</script>
<template>
<transition
:name="name"
@before-appear="beforeAppear"
@appear="appear"
@after-appear="afterAppear"
@appear-cancelled="appearCancelled"
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<slot />
</transition>
</template>

View file

@ -1,25 +0,0 @@
import type { Story } from '@storybook/vue3'
import Collapsible from './Collapsible.vue'
export default {
title: 'Components/Collapsible',
component: Collapsible,
args: {
modelValue: false,
title: 'Item',
content: 'lorem ipsum dolor sit amet',
},
}
const Template: Story = (args, { argTypes }) => ({
components: { Collapsible },
setup() {
return { args, argTypes }
},
template: `
<Collapsible v-bind="args"/>
`,
})
export const Default = Template.bind({})
Default.args = {}

View file

@ -1,91 +0,0 @@
<script lang="ts" setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import { ref, toRefs, watch } from 'vue'
import CollapseTransition from './CollapseTransition.vue'
import Modal from '../Modal/Modal.vue';
const props = withDefaults(
defineProps<{
modelValue?: boolean
title: string
content?: string
classes?: {
wrapper?: string
button?: string
title?: string
panel?: string
}
}>(),
{
modelValue: false,
},
)
const emit = defineEmits([
'update:modelValue',
'change',
'toggle',
'open',
'close',
])
const { modelValue } = toRefs(props)
const isOpen = ref(modelValue.value)
watch(modelValue, (val) => {
isOpen.value = val
})
watch(isOpen, (val) => {
emit('update:modelValue', val)
emit('change', val)
if (val)
emit('open')
else
emit('close')
})
const toggle = () => {
emit('toggle')
isOpen.value = !isOpen.value
}
</script>
<template>
<Disclosure v-slot="{ open }" as="div">
<DisclosureButton
class="
flex
items-center
justify-between
w-full
text-left
rounded-lg
focus:outline-none
focus-visible:ring
focus-visible:ring-blue-50
focus-visible:ring-opacity-75
"
:class="classes?.button"
type="button"
@click="toggle"
>
<div class="inline-flex w-full">
<Icon
name="heroicons:chevron-down"
:class="isOpen ? 'transform rotate-180' : ''"
class="w-5 h-5 text-black"
/>
<slot name="title"></slot>
</div>
</DisclosureButton>
<CollapseTransition>
<div v-show="isOpen">
<DisclosurePanel static class="pb-2 text-15" :class="classes?.panel">
<slot name="content"></slot>
</DisclosurePanel>
</div>
</CollapseTransition>
</Disclosure>
</template>

View file

@ -1,38 +0,0 @@
import type { Story } from '@storybook/vue3'
import CollapsibleGroup from './CollapsibleGroup.vue'
const genItems = (length = 5): any[] =>
Array.from({ length }, (_, v) => ({
title: `Item ${v + 1}`,
content: `lorem ipsum ${v + 1}`,
}))
const items = genItems(5)
export default {
title: 'Components/CollapsibleGroup',
component: CollapsibleGroup,
args: {
modelValue: false,
accordion: false,
items,
},
}
const Template: Story = (args, { argTypes }) => ({
components: { CollapsibleGroup },
setup() {
return { args, argTypes }
},
template: `
<CollapsibleGroup v-bind="args"/>
`,
})
export const Default = Template.bind({})
Default.args = {}
export const Accordion = Template.bind({})
Accordion.args = {
accordion: true,
}

View file

@ -1,54 +0,0 @@
<script lang="ts" setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
import { ref, toRefs, watch } from 'vue'
import Collapsible from './Collapsible.vue'
interface CollapsibleItem {
title: string
content: string
isOpen?: boolean
}
const props
= defineProps<{
items?: CollapsibleItem[]
classes?: {
wrapper?: string
button?: string
title?: string
panel?: string
}
accordion?: boolean
}>()
const { items } = toRefs(props)
const children = ref(props.items)
watch(items, (val) => {
children.value = val
})
const onToggle = (item: CollapsibleItem) => {
if (props.accordion) {
children.value.forEach((child) => {
child.isOpen = false
})
item.isOpen = true
}
}
</script>
<template>
<div class="w-full p-2" :class="classes?.wrapper">
<slot>
<Collapsible
v-for="(item, idx) in children"
:key="idx"
v-bind="item"
v-model="item.isOpen"
@toggle="onToggle(item)"
/>
</slot>
</div>
</template>

View file

@ -1,3 +0,0 @@
<template>
<div class="fixed inset-0 bg-black/50 z-15 transition duration-300" />
</template>

View file

@ -1,113 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import {
Dialog,
DialogPanel,
TransitionChild,
TransitionRoot,
} from '@headlessui/vue'
const props = withDefaults(
defineProps<{
modelValue?: boolean
persistent?: boolean
fullscreen?: boolean
maxHeight?: string
}>(),
{
modelValue: false,
persistent: false,
fullscreen: false,
maxHeight: '85vh',
},
)
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
const { modelValue } = toRefs(props)
const isOpen = ref(modelValue.value)
watch(modelValue, (value) => {
isOpen.value = value
})
function closeModal() {
isOpen.value = false
}
function openModal() {
isOpen.value = true
}
function onModalClose() {
if (!props.persistent)
closeModal()
}
watch(isOpen, (value) => {
emit('update:modelValue', value)
})
const api = {
isOpen,
open: openModal,
close: closeModal,
}
provide('modal', api)
</script>
<template>
<slot name="activator" :open="openModal" :on="{ click: openModal }" />
<TransitionRoot appear :show="isOpen" as="template">
<Dialog as="div" class="relative z-20" @close="onModalClose">
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div class="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div class="fixed inset-0 overflow-y-auto">
<div
class="flex min-h-full items-center justify-center text-center"
:class="{
'p-4': !fullscreen,
}"
>
<TransitionChild
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<DialogPanel
class="w-full transform overflow-hidden bg-white text-left align-middle shadow-xl transition-all py-4"
:class="{
'h-screen': fullscreen,
'max-w-[min(85vw,1200px)] rounded-lg': !fullscreen,
}"
:style="!fullscreen ? { maxHeight } : {}"
>
<slot />
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>

View file

@ -1,9 +0,0 @@
<script setup lang="ts">
import { DialogDescription } from '@headlessui/vue'
</script>
<template>
<DialogDescription class="px-4 py-3 text-sm text-gray-800">
<slot />
</DialogDescription>
</template>

View file

@ -1,9 +0,0 @@
<script setup lang="ts">
// import { ref } from 'vue'
</script>
<template>
<div class="px-4 py-3">
<slot />
</div>
</template>

View file

@ -1,34 +0,0 @@
<script setup lang="ts">
import { DialogTitle } from '@headlessui/vue'
interface Props {
dismissable?: boolean
titleClass?: string
}
defineProps<Props>()
const api = inject('modal')
</script>
<template>
<DialogTitle
as="div"
class="flex gap-2 justify-between items-center px-4 pt-3"
>
<h3
class="text-lg font-medium leading-6 text-gray-900"
:class="titleClass"
>
<slot />
</h3>
<slot v-if="dismissable" name="dismissable">
<button
class="text-2xl text-gray-500 appearance-none px-2 -mr-2"
@click="api.close"
>
&times;
</button>
</slot>
</DialogTitle>
</template>

View file

@ -1,111 +0,0 @@
<template>
<div class="bg-zinc-100 rounded-lg m-5 grid grid-cols-2 gap-10 divide-x divide-solid divide-black py-4 mb-10">
<div class="px-5">
<p class="text-lg">performances</p>
<div v-for="(item, index) in events">
<Collapsible title='placeholder' :modelValue='index <= 10' class="leading-tight py-2 ml-3 text-sm">
<template v-slot:title>
<div class="gap-1 w-[95%] px-2">
<div>
{{ item.formatted_date }}: {{item.venue.city}}, {{item.venue.state}}
<div class="ml-4 text-[#7F7F7F]">
{{ item.venue.name }}
</div>
</div>
</div>
</template>
<template v-slot:content>
<div v-for="performance in item.program">
<div class="italic text-sm ml-16 pt-1">{{performance.work}}</div>
<div v-if="performance.ensemble" class="ml-20">
{{ performance.ensemble }}
</div>
<div v-for="performer in performance.performers" class="ml-20">
{{ performer.name }}<span v-if="performer.instrument_tags?.length"> - </span>
<span v-for="(instrument, index) in performer.instrument_tags">
<span v-if="index !== 0">, </span>
{{ instrument }}
</span>
</div>
</div>
</template>
</Collapsible>
</div>
</div>
<div class="px-5">
<p class="text-lg">lectures</p>
<div v-for="yearGroup in lecturesByYear" :key="yearGroup.year">
<p class="text-sm font-semibold mt-4 text-[#7F7F7F]">{{ yearGroup.year }}</p>
<div class="leading-tight py-2 ml-3 text-sm" v-for="item in yearGroup.talks">
<div class="gap-1">
<div>
{{item.location}}
<div v-for="talk in item.talks" class="ml-4 text-[#7F7F7F]">
{{ talk.title }}
</div>
</div>
</div>
</div>
</div>
</div>
</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 { 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 - Events - Performances and Lectures'
})
</script>