Add iCal download for calendar integration #43
@@ -317,6 +317,72 @@ input[type="datetime-local"].form-field.glass::-webkit-datetime-edit-fields-wrap
|
||||
to { --glow-angle: 360deg; }
|
||||
}
|
||||
|
||||
/* ── Fixed Bottom Bar Components ── */
|
||||
|
||||
/* CTA wrapper (text button, e.g. "I'm attending!", "Post an update") */
|
||||
.bar-cta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: var(--radius-button);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.bar-cta:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.bar-cta:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.bar-cta-btn {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-radius: calc(var(--radius-button) - 2px);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-on-gradient);
|
||||
text-align: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* Icon wrapper (e.g. calendar, bookmark buttons) */
|
||||
.bar-icon {
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-button);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.bar-icon:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.bar-icon:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.bar-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--spacing-md);
|
||||
border-radius: calc(var(--radius-button) - 2px);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-on-gradient);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.bar-icon-btn svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<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"
|
||||
@@ -18,6 +19,15 @@
|
||||
<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"
|
||||
@@ -32,14 +42,9 @@
|
||||
|
||||
<!-- CTA state: no RSVP yet -->
|
||||
<div v-else class="rsvp-bar__row">
|
||||
<div class="rsvp-bar__cta glow-border glow-border--animated">
|
||||
<button class="rsvp-bar__cta-inner glass-inner" type="button" @click="$emit('open')">
|
||||
I'm attending!
|
||||
</button>
|
||||
</div>
|
||||
<div class="rsvp-bar__bookmark glow-border glow-border--animated">
|
||||
<div class="bar-icon glow-border glow-border--animated">
|
||||
<button
|
||||
class="rsvp-bar__bookmark-inner glass-inner"
|
||||
class="bar-icon-btn glass-inner"
|
||||
type="button"
|
||||
:aria-label="bookmarked ? 'Stop watching this event' : 'Watch this event'"
|
||||
@click="$emit('bookmark')"
|
||||
@@ -47,6 +52,21 @@
|
||||
<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>
|
||||
@@ -64,6 +84,7 @@ defineEmits<{
|
||||
open: []
|
||||
cancel: []
|
||||
bookmark: []
|
||||
calendar: []
|
||||
}>()
|
||||
|
||||
const expanded = ref(false)
|
||||
@@ -111,34 +132,6 @@ watch(expanded, (isExpanded) => {
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.rsvp-bar__cta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-radius: var(--radius-button);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.rsvp-bar__cta:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.rsvp-bar__cta:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.rsvp-bar__cta-inner {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-radius: calc(var(--radius-button) - 2px);
|
||||
font-family: inherit;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-on-gradient);
|
||||
text-align: center;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rsvp-bar__status-wrapper {
|
||||
display: flex;
|
||||
@@ -146,7 +139,14 @@ watch(expanded, (isExpanded) => {
|
||||
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;
|
||||
@@ -225,35 +225,35 @@ watch(expanded, (isExpanded) => {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.rsvp-bar__bookmark {
|
||||
|
||||
/* Calendar button — glassmorphic variant (post-RSVP status row) */
|
||||
.rsvp-bar__calendar-glass {
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-button);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.rsvp-bar__bookmark:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.rsvp-bar__bookmark:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.rsvp-bar__bookmark-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--spacing-md);
|
||||
border-radius: calc(var(--radius-button) - 2px);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
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__bookmark-inner svg {
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import RsvpBar from '../RsvpBar.vue'
|
||||
describe('RsvpBar', () => {
|
||||
it('renders CTA button when hasRsvp is false', () => {
|
||||
const wrapper = mount(RsvpBar)
|
||||
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
|
||||
expect(wrapper.find('.rsvp-bar__cta-inner').text()).toBe("I'm attending!")
|
||||
expect(wrapper.find('.bar-cta').exists()).toBe(true)
|
||||
expect(wrapper.find('.bar-cta-btn').text()).toBe("I'm attending!")
|
||||
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
|
||||
})
|
||||
|
||||
@@ -14,17 +14,17 @@ describe('RsvpBar', () => {
|
||||
const wrapper = mount(RsvpBar, { props: { hasRsvp: true } })
|
||||
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(true)
|
||||
expect(wrapper.find('.rsvp-bar__text').text()).toBe("You're attending!")
|
||||
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(false)
|
||||
expect(wrapper.find('.bar-cta').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('emits open when CTA button is clicked', async () => {
|
||||
const wrapper = mount(RsvpBar)
|
||||
await wrapper.find('.rsvp-bar__cta-inner').trigger('click')
|
||||
await wrapper.find('.bar-cta-btn').trigger('click')
|
||||
expect(wrapper.emitted('open')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not render CTA button when hasRsvp is true', () => {
|
||||
const wrapper = mount(RsvpBar, { props: { hasRsvp: true } })
|
||||
expect(wrapper.find('button').exists()).toBe(false)
|
||||
expect(wrapper.find('.bar-cta-btn').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user