Extract BackLink component into App layout
Move back navigation (chevron + "fete" brand) from per-view definitions into a shared BackLink component rendered in App.vue. Shown on all pages except home. Hero overlay gets pointer-events: none so the link stays clickable on the event detail page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,7 +109,7 @@ 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('Attending')).toBeVisible()
|
await expect(page.getByText('Attending')).toBeVisible()
|
||||||
await expect(page.getByText('Watching')).not.toBeVisible()
|
await expect(page.getByText('Watching')).not.toBeVisible()
|
||||||
})
|
})
|
||||||
@@ -137,7 +137,7 @@ 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('Attending')).not.toBeVisible()
|
await expect(page.getByText('Attending')).not.toBeVisible()
|
||||||
})
|
})
|
||||||
@@ -211,7 +211,7 @@ 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('Attending')).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>
|
||||||
|
|||||||
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>
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="create">
|
<main class="create">
|
||||||
<header class="create__header">
|
<h1 class="create__title">Create</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">
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user