Files
fete/frontend/src/components/RsvpBar.vue
2026-03-13 21:40:12 +01:00

261 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="rsvp-bar">
<div class="rsvp-bar__inner">
<!-- Status state: already RSVPed -->
<div v-if="hasRsvp" class="rsvp-bar__status-wrapper">
<div class="rsvp-bar__status-row">
<div
class="rsvp-bar__status"
role="button"
tabindex="0"
:aria-expanded="expanded"
aria-label="You're attending. Tap to show cancel option."
@click="expanded = !expanded"
@keydown.enter.prevent="expanded = !expanded"
@keydown.space.prevent="expanded = !expanded"
@keydown.escape="expanded = false"
>
<span class="rsvp-bar__check" aria-hidden="true"></span>
<span class="rsvp-bar__text">You're attending!</span>
<span class="rsvp-bar__chevron" :class="{ 'rsvp-bar__chevron--open': expanded }" aria-hidden="true"></span>
</div>
<button
class="rsvp-bar__calendar-glass"
type="button"
aria-label="Add to calendar"
@click="$emit('calendar')"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</button>
</div>
<Transition name="rsvp-bar-cancel">
<button
v-if="expanded"
class="rsvp-bar__cancel"
type="button"
@click="$emit('cancel')"
>
Cancel RSVP
</button>
</Transition>
</div>
<!-- CTA state: no RSVP yet -->
<div v-else class="rsvp-bar__row">
<div class="bar-icon glow-border glow-border--animated">
<button
class="bar-icon-btn glass-inner"
type="button"
:aria-label="bookmarked ? 'Stop watching this event' : 'Watch this event'"
@click="$emit('bookmark')"
>
<svg width="20" height="20" viewBox="0 0 24 24" :fill="bookmarked ? 'currentColor' : 'none'" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
</button>
</div>
<div class="bar-cta glow-border glow-border--animated">
<button class="bar-cta-btn glass-inner" type="button" @click="$emit('open')">
I'm attending!
</button>
</div>
<div class="bar-icon glow-border glow-border--animated">
<button
class="bar-icon-btn glass-inner"
type="button"
aria-label="Add to calendar"
@click="$emit('calendar')"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps<{
hasRsvp?: boolean
bookmarked?: boolean
}>()
defineEmits<{
open: []
cancel: []
bookmark: []
calendar: []
}>()
const expanded = ref(false)
watch(() => props.hasRsvp, () => {
expanded.value = false
})
function onClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.closest('.rsvp-bar__status-wrapper')) {
expanded.value = false
}
}
watch(expanded, (isExpanded) => {
if (isExpanded) {
document.addEventListener('click', onClickOutside, { capture: true })
} else {
document.removeEventListener('click', onClickOutside, { capture: true })
}
})
</script>
<style scoped>
.rsvp-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
z-index: 50;
padding: var(--spacing-md) var(--content-padding);
padding-bottom: calc(var(--spacing-md) + env(safe-area-inset-bottom, 0px));
}
.rsvp-bar__inner {
width: 100%;
max-width: var(--content-max-width);
}
.rsvp-bar__row {
display: flex;
gap: var(--spacing-sm);
}
.rsvp-bar__status-wrapper {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.rsvp-bar__status-row {
display: flex;
gap: var(--spacing-sm);
}
.rsvp-bar__status {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
border: 1px solid var(--color-glass-border);
backdrop-filter: blur(16px);
border-radius: var(--radius-card);
padding: var(--spacing-md) var(--spacing-lg);
box-shadow: var(--shadow-card);
font-weight: 600;
font-size: 0.95rem;
color: var(--color-text-on-gradient);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.rsvp-bar__status:hover {
background: linear-gradient(135deg, var(--color-glass-hover) 0%, var(--color-glass-strong) 100%);
}
.rsvp-bar__check {
color: #4caf50;
font-size: 1.1rem;
font-weight: 700;
}
.rsvp-bar__text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rsvp-bar__chevron {
font-size: 1.2rem;
font-weight: 700;
transition: transform 0.2s ease;
transform: rotate(0deg);
margin-left: auto;
}
.rsvp-bar__chevron--open {
transform: rotate(90deg);
}
.rsvp-bar__cancel {
display: block;
width: 100%;
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius-card);
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
color: #ef5350;
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
border: 1px solid var(--color-glass-border);
backdrop-filter: blur(16px);
cursor: pointer;
text-align: center;
transition: background 0.15s ease;
}
.rsvp-bar__cancel:hover {
background: linear-gradient(135deg, var(--color-glass-hover) 0%, var(--color-glass-strong) 100%);
}
.rsvp-bar-cancel-enter-active,
.rsvp-bar-cancel-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.rsvp-bar-cancel-enter-from,
.rsvp-bar-cancel-leave-to {
opacity: 0;
transform: translateY(-4px);
}
/* Calendar button — glassmorphic variant (post-RSVP status row) */
.rsvp-bar__calendar-glass {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-md);
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
border: 1px solid var(--color-glass-border);
backdrop-filter: blur(16px);
border-radius: var(--radius-card);
box-shadow: var(--shadow-card);
color: var(--color-text-on-gradient);
cursor: pointer;
line-height: 0;
transition: transform 0.1s ease, background 0.15s ease;
}
.rsvp-bar__calendar-glass:hover {
transform: scale(1.02);
background: linear-gradient(135deg, var(--color-glass-hover) 0%, var(--color-glass-strong) 100%);
}
.rsvp-bar__calendar-glass:active {
transform: scale(0.98);
}
.rsvp-bar__calendar-glass svg {
display: block;
}
</style>