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; }
|
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 */
|
/* Utility */
|
||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -3,20 +3,30 @@
|
|||||||
<div class="rsvp-bar__inner">
|
<div class="rsvp-bar__inner">
|
||||||
<!-- Status state: already RSVPed -->
|
<!-- Status state: already RSVPed -->
|
||||||
<div v-if="hasRsvp" class="rsvp-bar__status-wrapper">
|
<div v-if="hasRsvp" class="rsvp-bar__status-wrapper">
|
||||||
<div
|
<div class="rsvp-bar__status-row">
|
||||||
class="rsvp-bar__status"
|
<div
|
||||||
role="button"
|
class="rsvp-bar__status"
|
||||||
tabindex="0"
|
role="button"
|
||||||
:aria-expanded="expanded"
|
tabindex="0"
|
||||||
aria-label="You're attending. Tap to show cancel option."
|
:aria-expanded="expanded"
|
||||||
@click="expanded = !expanded"
|
aria-label="You're attending. Tap to show cancel option."
|
||||||
@keydown.enter.prevent="expanded = !expanded"
|
@click="expanded = !expanded"
|
||||||
@keydown.space.prevent="expanded = !expanded"
|
@keydown.enter.prevent="expanded = !expanded"
|
||||||
@keydown.escape="expanded = false"
|
@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__check" aria-hidden="true">✓</span>
|
||||||
<span class="rsvp-bar__chevron" :class="{ 'rsvp-bar__chevron--open': expanded }" 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>
|
</div>
|
||||||
<Transition name="rsvp-bar-cancel">
|
<Transition name="rsvp-bar-cancel">
|
||||||
<button
|
<button
|
||||||
@@ -32,14 +42,9 @@
|
|||||||
|
|
||||||
<!-- CTA state: no RSVP yet -->
|
<!-- CTA state: no RSVP yet -->
|
||||||
<div v-else class="rsvp-bar__row">
|
<div v-else class="rsvp-bar__row">
|
||||||
<div class="rsvp-bar__cta glow-border glow-border--animated">
|
<div class="bar-icon 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">
|
|
||||||
<button
|
<button
|
||||||
class="rsvp-bar__bookmark-inner glass-inner"
|
class="bar-icon-btn glass-inner"
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="bookmarked ? 'Stop watching this event' : 'Watch this event'"
|
:aria-label="bookmarked ? 'Stop watching this event' : 'Watch this event'"
|
||||||
@click="$emit('bookmark')"
|
@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>
|
<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>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,6 +84,7 @@ defineEmits<{
|
|||||||
open: []
|
open: []
|
||||||
cancel: []
|
cancel: []
|
||||||
bookmark: []
|
bookmark: []
|
||||||
|
calendar: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const expanded = ref(false)
|
const expanded = ref(false)
|
||||||
@@ -111,34 +132,6 @@ watch(expanded, (isExpanded) => {
|
|||||||
gap: var(--spacing-sm);
|
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 {
|
.rsvp-bar__status-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -146,7 +139,14 @@ watch(expanded, (isExpanded) => {
|
|||||||
gap: var(--spacing-xs);
|
gap: var(--spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rsvp-bar__status-row {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.rsvp-bar__status {
|
.rsvp-bar__status {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -225,35 +225,35 @@ watch(expanded, (isExpanded) => {
|
|||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsvp-bar__bookmark {
|
|
||||||
|
/* Calendar button — glassmorphic variant (post-RSVP status row) */
|
||||||
|
.rsvp-bar__calendar-glass {
|
||||||
flex-shrink: 0;
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: var(--spacing-md);
|
padding: var(--spacing-md);
|
||||||
border-radius: calc(var(--radius-button) - 2px);
|
background: linear-gradient(135deg, var(--color-glass-strong) 0%, var(--color-glass-subtle) 100%);
|
||||||
border: none;
|
border: 1px solid var(--color-glass-border);
|
||||||
cursor: pointer;
|
backdrop-filter: blur(16px);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
color: var(--color-text-on-gradient);
|
color: var(--color-text-on-gradient);
|
||||||
|
cursor: pointer;
|
||||||
line-height: 0;
|
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;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import RsvpBar from '../RsvpBar.vue'
|
|||||||
describe('RsvpBar', () => {
|
describe('RsvpBar', () => {
|
||||||
it('renders CTA button when hasRsvp is false', () => {
|
it('renders CTA button when hasRsvp is false', () => {
|
||||||
const wrapper = mount(RsvpBar)
|
const wrapper = mount(RsvpBar)
|
||||||
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
|
expect(wrapper.find('.bar-cta').exists()).toBe(true)
|
||||||
expect(wrapper.find('.rsvp-bar__cta-inner').text()).toBe("I'm attending!")
|
expect(wrapper.find('.bar-cta-btn').text()).toBe("I'm attending!")
|
||||||
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
|
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -14,17 +14,17 @@ describe('RsvpBar', () => {
|
|||||||
const wrapper = mount(RsvpBar, { props: { hasRsvp: true } })
|
const wrapper = mount(RsvpBar, { props: { hasRsvp: true } })
|
||||||
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(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__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 () => {
|
it('emits open when CTA button is clicked', async () => {
|
||||||
const wrapper = mount(RsvpBar)
|
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)
|
expect(wrapper.emitted('open')).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not render CTA button when hasRsvp is true', () => {
|
it('does not render CTA button when hasRsvp is true', () => {
|
||||||
const wrapper = mount(RsvpBar, { props: { hasRsvp: 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