Compare commits
9 Commits
c51eacb261
...
0.12.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 51ab99fc61 | |||
| d52f51d6e1 | |||
| c1760ae376 | |||
| 6d51327e56 | |||
| 96044ae1ed | |||
| f972a41e45 | |||
| 13b01dfba8 | |||
| fd8724db8f | |||
| 8885dbd722 |
@@ -43,7 +43,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
|
|||||||
await expect(statusBar).toBeVisible()
|
await expect(statusBar).toBeVisible()
|
||||||
|
|
||||||
// Cancel button hidden initially
|
// Cancel button hidden initially
|
||||||
await expect(page.getByRole('button', { name: 'Cancel attendance' })).not.toBeVisible()
|
await expect(page.getByRole('button', { name: 'Cancel RSVP' })).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('tapping status bar reveals cancel button', async ({ page, network }) => {
|
test('tapping status bar reveals cancel button', async ({ page, network }) => {
|
||||||
@@ -57,7 +57,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
|
|||||||
await page.getByRole('button', { name: /You're attending/ }).click()
|
await page.getByRole('button', { name: /You're attending/ }).click()
|
||||||
|
|
||||||
// Cancel button appears
|
// Cancel button appears
|
||||||
await expect(page.getByRole('button', { name: 'Cancel attendance' })).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Cancel RSVP' })).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('confirm cancellation → localStorage cleared, count decremented, bar reset', async ({ page, network }) => {
|
test('confirm cancellation → localStorage cleared, count decremented, bar reset', async ({ page, network }) => {
|
||||||
@@ -70,13 +70,13 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
|
|||||||
await page.addInitScript(seedEvents([rsvpSeed()]))
|
await page.addInitScript(seedEvents([rsvpSeed()]))
|
||||||
await page.goto(`/events/${fullEvent.eventToken}`)
|
await page.goto(`/events/${fullEvent.eventToken}`)
|
||||||
|
|
||||||
// Expand → Cancel attendance → Confirm in dialog
|
// Expand → Cancel RSVP → Confirm in dialog
|
||||||
await page.getByRole('button', { name: /You're attending/ }).click()
|
await page.getByRole('button', { name: /You're attending/ }).click()
|
||||||
await page.locator('.rsvp-bar__cancel').click()
|
await page.locator('.rsvp-bar__cancel').click()
|
||||||
|
|
||||||
// Confirm dialog
|
// Confirm dialog
|
||||||
await expect(page.getByText('Your attendance will be permanently cancelled.')).toBeVisible()
|
await expect(page.getByText('The organizer will no longer see you as attending.')).toBeVisible()
|
||||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click()
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
|
||||||
|
|
||||||
// Bar resets to CTA state
|
// Bar resets to CTA state
|
||||||
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
|
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
|
||||||
@@ -108,7 +108,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
|
|||||||
// Expand → Cancel → Confirm in dialog
|
// Expand → Cancel → Confirm in dialog
|
||||||
await page.getByRole('button', { name: /You're attending/ }).click()
|
await page.getByRole('button', { name: /You're attending/ }).click()
|
||||||
await page.locator('.rsvp-bar__cancel').click()
|
await page.locator('.rsvp-bar__cancel').click()
|
||||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click()
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
|
||||||
|
|
||||||
// Error message
|
// Error message
|
||||||
await expect(page.getByText('Could not cancel RSVP. Please try again.')).toBeVisible()
|
await expect(page.getByText('Could not cancel RSVP. Please try again.')).toBeVisible()
|
||||||
@@ -136,7 +136,7 @@ test.describe('US1: Cancel RSVP from Event Detail View', () => {
|
|||||||
// Cancel first
|
// Cancel first
|
||||||
await page.getByRole('button', { name: /You're attending/ }).click()
|
await page.getByRole('button', { name: /You're attending/ }).click()
|
||||||
await page.locator('.rsvp-bar__cancel').click()
|
await page.locator('.rsvp-bar__cancel').click()
|
||||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click()
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
|
||||||
|
|
||||||
// CTA should be back
|
// CTA should be back
|
||||||
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
|
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
|
||||||
@@ -244,7 +244,7 @@ test.describe('US3: Cancel RSVP with Stale/Invalid Token', () => {
|
|||||||
// Cancel flow
|
// Cancel flow
|
||||||
await page.getByRole('button', { name: /You're attending/ }).click()
|
await page.getByRole('button', { name: /You're attending/ }).click()
|
||||||
await page.locator('.rsvp-bar__cancel').click()
|
await page.locator('.rsvp-bar__cancel').click()
|
||||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click()
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
|
||||||
|
|
||||||
// Treated as success — CTA returns
|
// Treated as success — CTA returns
|
||||||
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
|
await expect(page.getByRole('button', { name: "I'm attending" })).toBeVisible()
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ test.describe('US5: Visual Distinction for Event Roles', () => {
|
|||||||
const card = page.locator('.event-card').filter({ hasText: 'Summer BBQ' })
|
const card = page.locator('.event-card').filter({ hasText: 'Summer BBQ' })
|
||||||
const badge = card.locator('.event-card__badge')
|
const badge = card.locator('.event-card__badge')
|
||||||
await expect(badge).toBeVisible()
|
await expect(badge).toBeVisible()
|
||||||
await expect(badge).toHaveText('Organizer')
|
await expect(badge).toHaveText('Organizing')
|
||||||
await expect(badge).toHaveClass(/event-card__badge--organizer/)
|
await expect(badge).toHaveClass(/event-card__badge--organizer/)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ test.describe('US5: Visual Distinction for Event Roles', () => {
|
|||||||
const card = page.locator('.event-card').filter({ hasText: 'Team Meeting' })
|
const card = page.locator('.event-card').filter({ hasText: 'Team Meeting' })
|
||||||
const badge = card.locator('.event-card__badge')
|
const badge = card.locator('.event-card__badge')
|
||||||
await expect(badge).toBeVisible()
|
await expect(badge).toBeVisible()
|
||||||
await expect(badge).toHaveText('Attendee')
|
await expect(badge).toHaveText('Attending')
|
||||||
await expect(badge).toHaveClass(/event-card__badge--attendee/)
|
await expect(badge).toHaveClass(/event-card__badge--attendee/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ test.describe('US1: Watch event from detail page', () => {
|
|||||||
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
||||||
|
|
||||||
// Navigate to event list via back link
|
// Navigate to event list via back link
|
||||||
await page.locator('.detail__back').click()
|
await page.getByLabel('Back to home').click()
|
||||||
|
|
||||||
// Event appears with "Watching" label
|
// Event appears with "Watching" label
|
||||||
await expect(page.getByText('Summer BBQ')).toBeVisible()
|
await expect(page.getByText('Summer BBQ')).toBeVisible()
|
||||||
@@ -89,7 +89,7 @@ test.describe('US2: Un-watch event from detail page', () => {
|
|||||||
await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event')
|
await expect(bookmark).toHaveAttribute('aria-label', 'Watch this event')
|
||||||
|
|
||||||
// Navigate to event list via back link (avoid page.goto re-running addInitScript)
|
// Navigate to event list via back link (avoid page.goto re-running addInitScript)
|
||||||
await page.locator('.detail__back').click()
|
await page.getByLabel('Back to home').click()
|
||||||
|
|
||||||
// Event is gone
|
// Event is gone
|
||||||
await expect(page.getByText('Summer BBQ')).not.toBeVisible()
|
await expect(page.getByText('Summer BBQ')).not.toBeVisible()
|
||||||
@@ -109,8 +109,8 @@ test.describe('US3: Bookmark reflects attending status', () => {
|
|||||||
await expect(bookmark).not.toBeVisible()
|
await expect(bookmark).not.toBeVisible()
|
||||||
|
|
||||||
// Navigate to list via back link
|
// Navigate to list via back link
|
||||||
await page.locator('.detail__back').click()
|
await page.getByLabel('Back to home').click()
|
||||||
await expect(page.getByText('Attendee')).toBeVisible()
|
await expect(page.getByText('Attending')).toBeVisible()
|
||||||
await expect(page.getByText('Watching')).not.toBeVisible()
|
await expect(page.getByText('Watching')).not.toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -129,7 +129,7 @@ test.describe('US4: RSVP cancellation preserves watch status', () => {
|
|||||||
// Cancel RSVP
|
// Cancel RSVP
|
||||||
await page.getByRole('button', { name: /You're attending/ }).click()
|
await page.getByRole('button', { name: /You're attending/ }).click()
|
||||||
await page.locator('.rsvp-bar__cancel').click()
|
await page.locator('.rsvp-bar__cancel').click()
|
||||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel attendance' }).click()
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Cancel RSVP' }).click()
|
||||||
|
|
||||||
// Bookmark reappears in CTA state, filled because event is still stored
|
// Bookmark reappears in CTA state, filled because event is still stored
|
||||||
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
|
const bookmark = page.locator('.rsvp-bar__bookmark-inner')
|
||||||
@@ -137,9 +137,9 @@ test.describe('US4: RSVP cancellation preserves watch status', () => {
|
|||||||
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
await expect(bookmark).toHaveAttribute('aria-label', 'Stop watching this event')
|
||||||
|
|
||||||
// Navigate to list via back link
|
// Navigate to list via back link
|
||||||
await page.locator('.detail__back').click()
|
await page.getByLabel('Back to home').click()
|
||||||
await expect(page.getByText('Watching')).toBeVisible()
|
await expect(page.getByText('Watching')).toBeVisible()
|
||||||
await expect(page.getByText('Attendee')).not.toBeVisible()
|
await expect(page.getByText('Attending')).not.toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -211,8 +211,8 @@ test.describe('US7: Watcher upgrades to attendee', () => {
|
|||||||
await expect(bookmark).not.toBeVisible()
|
await expect(bookmark).not.toBeVisible()
|
||||||
|
|
||||||
// Navigate to list via back link
|
// Navigate to list via back link
|
||||||
await page.locator('.detail__back').click()
|
await page.getByLabel('Back to home').click()
|
||||||
await expect(page.getByText('Attendee')).toBeVisible()
|
await expect(page.getByText('Attending')).toBeVisible()
|
||||||
await expect(page.getByText('Watching')).not.toBeVisible()
|
await expect(page.getByText('Watching')).not.toBeVisible()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
|
<header v-if="route.name !== 'home'" class="app-header">
|
||||||
|
<BackLink />
|
||||||
|
</header>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } from 'vue-router'
|
import { RouterView, useRoute } from 'vue-router'
|
||||||
|
import BackLink from '@/components/BackLink.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
padding-top: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
--color-danger-bg-strong: rgba(220, 38, 38, 0.2);
|
--color-danger-bg-strong: rgba(220, 38, 38, 0.2);
|
||||||
--color-danger-border: rgba(220, 38, 38, 0.3);
|
--color-danger-border: rgba(220, 38, 38, 0.3);
|
||||||
--color-danger-border-strong: rgba(220, 38, 38, 0.4);
|
--color-danger-border-strong: rgba(220, 38, 38, 0.4);
|
||||||
|
--color-danger-solid: #d32f2f;
|
||||||
|
--color-danger-solid-hover: #b71c1c;
|
||||||
|
--color-danger-solid-text: #fff;
|
||||||
|
|
||||||
/* Glass system */
|
/* Glass system */
|
||||||
--color-glass: rgba(255, 255, 255, 0.1);
|
--color-glass: rgba(255, 255, 255, 0.1);
|
||||||
@@ -214,7 +217,7 @@ textarea.form-field {
|
|||||||
|
|
||||||
/* Error message */
|
/* Error message */
|
||||||
.field-error {
|
.field-error {
|
||||||
color: #fff;
|
color: var(--color-danger-solid);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
@@ -325,7 +328,8 @@ textarea.form-field {
|
|||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rsvp-form__label {
|
.rsvp-form__label,
|
||||||
|
.cancel-form__label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-text-on-gradient);
|
color: var(--color-text-on-gradient);
|
||||||
@@ -333,7 +337,7 @@ textarea.form-field {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rsvp-form__field-error {
|
.rsvp-form__field-error {
|
||||||
color: #d32f2f;
|
color: var(--color-danger-solid);
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
|
|||||||
28
frontend/src/components/BackLink.vue
Normal file
28
frontend/src/components/BackLink.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<RouterLink to="/" class="back-link" aria-label="Back to home">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="15 18 9 12 15 6" />
|
||||||
|
</svg>
|
||||||
|
<span class="back-link__brand">fete</span>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.back-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.15rem;
|
||||||
|
color: var(--color-text-on-gradient);
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-link__brand {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,18 @@
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="sheet">
|
<Transition name="sheet">
|
||||||
<div v-if="open" class="sheet-backdrop" @click.self="$emit('close')" @keydown.escape="$emit('close')">
|
<div v-if="open" class="sheet-backdrop" @click.self="$emit('close')" @keydown.escape="$emit('close')">
|
||||||
<div class="sheet" role="dialog" aria-modal="true" :aria-label="label" ref="sheetEl" tabindex="-1">
|
<div
|
||||||
|
class="sheet"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
:aria-label="label"
|
||||||
|
ref="sheetEl"
|
||||||
|
tabindex="-1"
|
||||||
|
:style="dragStyle"
|
||||||
|
@touchstart="onTouchStart"
|
||||||
|
@touchmove="onTouchMove"
|
||||||
|
@touchend="onTouchEnd"
|
||||||
|
>
|
||||||
<div class="sheet__handle" aria-hidden="true" />
|
<div class="sheet__handle" aria-hidden="true" />
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -12,14 +23,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
label: string
|
label: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
const emit = defineEmits<{
|
||||||
close: []
|
close: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -39,6 +50,45 @@ watch(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/* ── Drag-to-dismiss ── */
|
||||||
|
const DISMISS_THRESHOLD = 100
|
||||||
|
const dragY = ref(0)
|
||||||
|
const dragging = ref(false)
|
||||||
|
let startY = 0
|
||||||
|
|
||||||
|
const dragStyle = computed(() => {
|
||||||
|
if (!dragging.value || dragY.value <= 0) return undefined
|
||||||
|
return {
|
||||||
|
transform: `translateY(${dragY.value}px)`,
|
||||||
|
transition: 'none',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onTouchStart(e: TouchEvent) {
|
||||||
|
const touch = e.touches[0]
|
||||||
|
if (!touch) return
|
||||||
|
startY = touch.clientY
|
||||||
|
dragging.value = true
|
||||||
|
dragY.value = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchMove(e: TouchEvent) {
|
||||||
|
if (!dragging.value) return
|
||||||
|
const touch = e.touches[0]
|
||||||
|
if (!touch) return
|
||||||
|
const delta = touch.clientY - startY
|
||||||
|
if (delta > 0) e.preventDefault()
|
||||||
|
dragY.value = Math.max(0, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTouchEnd() {
|
||||||
|
if (dragY.value >= DISMISS_THRESHOLD) {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
dragging.value = false
|
||||||
|
dragY.value = 0
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -139,8 +139,8 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog__btn--confirm {
|
.confirm-dialog__btn--confirm {
|
||||||
background: #d32f2f;
|
background: var(--color-danger-solid);
|
||||||
color: #fff;
|
color: var(--color-danger-solid-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirm-dialog-enter-active,
|
.confirm-dialog-enter-active,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<span class="event-card__time">{{ displayTime }}</span>
|
<span class="event-card__time">{{ displayTime }}</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<span v-if="eventRole" class="event-card__badge" :class="`event-card__badge--${eventRole}`">
|
<span v-if="eventRole" class="event-card__badge" :class="`event-card__badge--${eventRole}`">
|
||||||
{{ eventRole === 'organizer' ? 'Organizer' : eventRole === 'attendee' ? 'Attendee' : 'Watching' }}
|
{{ eventRole === 'organizer' ? 'Organizing' : eventRole === 'attendee' ? 'Attending' : 'Watching' }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="event-card__delete"
|
class="event-card__delete"
|
||||||
@@ -175,7 +175,7 @@ function onTouchEnd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.event-card__delete:hover {
|
.event-card__delete:hover {
|
||||||
color: #d32f2f;
|
color: var(--color-danger-solid);
|
||||||
background: rgba(211, 47, 47, 0.08);
|
background: rgba(211, 47, 47, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
@click="$emit('cancel')"
|
@click="$emit('cancel')"
|
||||||
>
|
>
|
||||||
Cancel attendance
|
Cancel RSVP
|
||||||
</button>
|
</button>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<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="rsvp-bar__cta glow-border glow-border--animated">
|
||||||
<button class="rsvp-bar__cta-inner glass-inner" type="button" @click="$emit('open')">
|
<button class="rsvp-bar__cta-inner glass-inner" type="button" @click="$emit('open')">
|
||||||
I'm attending
|
I'm attending!
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="rsvp-bar__bookmark glow-border glow-border--animated">
|
<div class="rsvp-bar__bookmark glow-border glow-border--animated">
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ describe('EventCard', () => {
|
|||||||
|
|
||||||
it('renders organizer badge when eventRole is organizer', () => {
|
it('renders organizer badge when eventRole is organizer', () => {
|
||||||
const wrapper = mountCard({ eventRole: 'organizer' })
|
const wrapper = mountCard({ eventRole: 'organizer' })
|
||||||
expect(wrapper.text()).toContain('Organizer')
|
expect(wrapper.text()).toContain('Organizing')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders attendee badge when eventRole is attendee', () => {
|
it('renders attendee badge when eventRole is attendee', () => {
|
||||||
const wrapper = mountCard({ eventRole: 'attendee' })
|
const wrapper = mountCard({ eventRole: 'attendee' })
|
||||||
expect(wrapper.text()).toContain('Attendee')
|
expect(wrapper.text()).toContain('Attending')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders watcher badge when eventRole is watcher', () => {
|
it('renders watcher badge when eventRole is watcher', () => {
|
||||||
|
|||||||
@@ -160,13 +160,13 @@ describe('EventList', () => {
|
|||||||
const wrapper = mountList()
|
const wrapper = mountList()
|
||||||
const badge = wrapper.find('.event-card__badge--organizer')
|
const badge = wrapper.find('.event-card__badge--organizer')
|
||||||
expect(badge.exists()).toBe(true)
|
expect(badge.exists()).toBe(true)
|
||||||
expect(badge.text()).toBe('Organizer')
|
expect(badge.text()).toBe('Organizing')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('assigns attendee role when event has rsvpToken', () => {
|
it('assigns attendee role when event has rsvpToken', () => {
|
||||||
const wrapper = mountList()
|
const wrapper = mountList()
|
||||||
const badge = wrapper.find('.event-card__badge--attendee')
|
const badge = wrapper.find('.event-card__badge--attendee')
|
||||||
expect(badge.exists()).toBe(true)
|
expect(badge.exists()).toBe(true)
|
||||||
expect(badge.text()).toBe('Attendee')
|
expect(badge.text()).toBe('Attending')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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('.rsvp-bar__cta').exists()).toBe(true)
|
||||||
expect(wrapper.find('.rsvp-bar__cta-inner').text()).toBe("I'm attending")
|
expect(wrapper.find('.rsvp-bar__cta-inner').text()).toBe("I'm attending!")
|
||||||
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
|
expect(wrapper.find('.rsvp-bar__status').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="create">
|
<main class="create">
|
||||||
<header class="create__header">
|
<h1 class="create__title">Great, a Party!</h1>
|
||||||
<RouterLink to="/" class="create__back" aria-label="Back to home">←</RouterLink>
|
|
||||||
<h1 class="create__title">Create</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<form class="create__form" novalidate @submit.prevent="handleSubmit">
|
<form class="create__form" novalidate @submit.prevent="handleSubmit">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -76,7 +73,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, watch } from 'vue'
|
import { reactive, ref, watch } from 'vue'
|
||||||
import { RouterLink, useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import { useEventStorage } from '@/composables/useEventStorage'
|
import { useEventStorage } from '@/composables/useEventStorage'
|
||||||
|
|
||||||
@@ -194,20 +191,7 @@ async function handleSubmit() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-lg);
|
gap: var(--spacing-lg);
|
||||||
padding-top: var(--spacing-lg);
|
padding-top: calc(var(--spacing-lg) + 2.5rem);
|
||||||
}
|
|
||||||
|
|
||||||
.create__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.create__back {
|
|
||||||
color: var(--color-text-on-gradient);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create__title {
|
.create__title {
|
||||||
|
|||||||
@@ -8,10 +8,6 @@
|
|||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
<div class="detail__hero-overlay" />
|
<div class="detail__hero-overlay" />
|
||||||
<header class="detail__header">
|
|
||||||
<RouterLink to="/" class="detail__back" aria-label="Back to home">←</RouterLink>
|
|
||||||
<span class="detail__brand">fete</span>
|
|
||||||
</header>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="detail__body">
|
<div class="detail__body">
|
||||||
@@ -129,9 +125,9 @@
|
|||||||
<!-- Cancel confirmation dialog -->
|
<!-- Cancel confirmation dialog -->
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
:open="confirmCancelOpen"
|
:open="confirmCancelOpen"
|
||||||
title="Cancel attendance?"
|
title="Cancel RSVP?"
|
||||||
message="Your attendance will be permanently cancelled."
|
message="The organizer will no longer see you as attending."
|
||||||
confirm-label="Cancel attendance"
|
confirm-label="Cancel RSVP"
|
||||||
cancel-label="Keep"
|
cancel-label="Keep"
|
||||||
@confirm="handleCancelRsvp"
|
@confirm="handleCancelRsvp"
|
||||||
@cancel="confirmCancelOpen = false"
|
@cancel="confirmCancelOpen = false"
|
||||||
@@ -168,7 +164,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { RouterLink, useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { api } from '@/api/client'
|
import { api } from '@/api/client'
|
||||||
import { useEventStorage } from '@/composables/useEventStorage'
|
import { useEventStorage } from '@/composables/useEventStorage'
|
||||||
import AttendeeList from '@/components/AttendeeList.vue'
|
import AttendeeList from '@/components/AttendeeList.vue'
|
||||||
@@ -437,32 +433,7 @@ onMounted(fetchEvent)
|
|||||||
var(--color-glass-overlay) 0%,
|
var(--color-glass-overlay) 0%,
|
||||||
transparent 50%
|
transparent 50%
|
||||||
);
|
);
|
||||||
}
|
pointer-events: none;
|
||||||
|
|
||||||
.detail__header {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
padding: var(--spacing-lg) var(--content-padding);
|
|
||||||
padding-top: env(safe-area-inset-top, var(--spacing-lg));
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail__back {
|
|
||||||
color: var(--color-text-on-gradient);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail__brand {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-text-on-gradient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail__body {
|
.detail__body {
|
||||||
@@ -706,15 +677,15 @@ onMounted(fetchEvent)
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-danger);
|
color: var(--color-danger-solid-text);
|
||||||
background: var(--color-danger-bg-strong);
|
background: var(--color-danger-solid);
|
||||||
border: 1px solid var(--color-danger-border);
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s ease;
|
transition: background 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-form__confirm:hover {
|
.cancel-form__confirm:hover {
|
||||||
background: var(--color-danger-bg-hover);
|
background: var(--color-danger-solid-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cancel-form__confirm:disabled {
|
.cancel-form__confirm:disabled {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ describe('EventDetailView', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
|
expect(wrapper.find('.rsvp-bar__cta').exists()).toBe(true)
|
||||||
expect(wrapper.find('.rsvp-bar__cta').text()).toBe("I'm attending")
|
expect(wrapper.find('.rsvp-bar__cta').text()).toBe("I'm attending!")
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user