ghostguild-org/app/layouts/admin.vue
Jennie Robinson Faber c40f2c7c63 fix: accessibility improvements and test infrastructure hardening
Add aria-labels to form controls (selects, checkboxes, switches), set
html lang attribute and page title, fix color contrast for --candle-dim
and --text-faint tokens, underline inline links, remove opacity hack.
Harden dev login endpoints with atomic findOneAndUpdate and tokenVersion
in JWT. Update Playwright timeouts and E2E test helpers.
2026-04-05 21:59:02 +01:00

260 lines
6.3 KiB
Vue

<template>
<div class="site">
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:z-[100] focus:p-3 focus:bg-[var(--bg)] focus:text-[var(--text)]">Skip to content</a>
<!-- Desktop Sidebar -->
<aside class="sidebar sidebar-desktop">
<NuxtLink to="/" class="sidebar-brand">Ghost Guild</NuxtLink>
<div class="sidebar-body">
<div class="sidebar-section">Admin</div>
<ul class="sidebar-nav">
<li>
<NuxtLink to="/admin" :class="{ active: route.path === '/admin' }">
Dashboard
</NuxtLink>
</li>
<li>
<NuxtLink to="/admin/members" :class="{ active: route.path.startsWith('/admin/members') }">
Members
</NuxtLink>
</li>
<li>
<NuxtLink to="/admin/events" :class="{ active: route.path.startsWith('/admin/events') }">
Events
</NuxtLink>
</li>
<li>
<NuxtLink to="/admin/series-management" :class="{ active: route.path.includes('/admin/series') }">
Series
</NuxtLink>
</li>
</ul>
<div class="sidebar-section">Site</div>
<ul class="sidebar-nav">
<li><NuxtLink to="/member/dashboard">Your Dashboard</NuxtLink></li>
<li><NuxtLink to="/">Public Site</NuxtLink></li>
</ul>
</div>
<div class="sidebar-meta">
<span class="admin-tag">admin</span><br>
<a href="#" @click.prevent="logout">Sign out</a>
</div>
</aside>
<!-- Mobile Header -->
<div class="mobile-header">
<NuxtLink to="/admin" class="brand">Ghost Guild</NuxtLink>
<button class="btn" @click="isMobileMenuOpen = true">Menu</button>
</div>
<!-- Main Content -->
<main id="main-content" class="main">
<TopStrip :page-path="currentPageName" />
<slot />
</main>
<!-- Mobile Drawer -->
<USlideover v-model:open="isMobileMenuOpen" side="left">
<template #body>
<aside class="sidebar sidebar-mobile">
<NuxtLink to="/" class="sidebar-brand" @click="isMobileMenuOpen = false">Ghost Guild</NuxtLink>
<div class="sidebar-body">
<div class="sidebar-section">Admin</div>
<ul class="sidebar-nav">
<li>
<NuxtLink to="/admin" :class="{ active: route.path === '/admin' }" @click="isMobileMenuOpen = false">
Dashboard
</NuxtLink>
</li>
<li>
<NuxtLink to="/admin/members" :class="{ active: route.path.startsWith('/admin/members') }" @click="isMobileMenuOpen = false">
Members
</NuxtLink>
</li>
<li>
<NuxtLink to="/admin/events" :class="{ active: route.path.startsWith('/admin/events') }" @click="isMobileMenuOpen = false">
Events
</NuxtLink>
</li>
<li>
<NuxtLink to="/admin/series-management" :class="{ active: route.path.includes('/admin/series') }" @click="isMobileMenuOpen = false">
Series
</NuxtLink>
</li>
</ul>
<div class="sidebar-section">Site</div>
<ul class="sidebar-nav">
<li><NuxtLink to="/member/dashboard" @click="isMobileMenuOpen = false">Your Dashboard</NuxtLink></li>
<li><NuxtLink to="/" @click="isMobileMenuOpen = false">Public Site</NuxtLink></li>
</ul>
</div>
<div class="sidebar-meta">
<span class="admin-tag">admin</span><br>
<a href="#" @click.prevent="logout">Sign out</a>
</div>
</aside>
</template>
</USlideover>
</div>
</template>
<script setup>
const route = useRoute()
const isMobileMenuOpen = ref(false)
const { logout } = useAuth()
const currentPageName = computed(() => {
const path = route.path
if (path === '/admin') return 'admin'
return path.slice(1).replace(/\//g, ' / ')
})
</script>
<style scoped>
.site {
min-height: 100vh;
}
.main {
margin-left: 220px;
}
/* ---- SIDEBAR ---- */
.sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: 220px;
display: flex;
flex-direction: column;
background: var(--bg);
border-right: 1px dashed var(--border);
z-index: 10;
}
.sidebar-mobile {
position: static;
width: 100%;
min-height: 100%;
border-right: none;
}
.sidebar-brand {
display: block;
font-family: 'Brygada 1918', serif;
font-size: 16px;
font-weight: 600;
color: var(--candle);
padding: 24px 24px 16px;
border-bottom: 1px dashed var(--border);
text-decoration: none;
}
.sidebar-brand:hover { text-decoration: none; }
.sidebar-body {
flex: 1;
overflow-y: auto;
}
.sidebar-section {
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-faint);
margin: 20px 0 8px;
padding: 0 24px;
}
.sidebar-nav {
list-style: none;
margin-bottom: 0;
padding: 0 14px;
}
.sidebar-nav li {
margin-bottom: 2px;
}
.sidebar-nav a {
display: block;
padding: 6px 10px;
color: var(--text-dim);
font-size: 13px;
transition: all 0.15s;
text-decoration: none;
}
.sidebar-nav a:hover {
color: var(--text);
background: var(--surface);
text-decoration: none;
}
.sidebar-nav a.active {
color: var(--text-bright);
background: var(--surface);
}
.sidebar-meta {
margin-top: auto;
font-size: 11px;
color: var(--text-faint);
line-height: 1.7;
padding: 16px 24px 24px;
border-top: 1px dashed var(--border);
}
.sidebar-meta a {
color: var(--candle-dim);
}
.admin-tag {
font-size: 10px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--candle);
}
/* ---- MOBILE ---- */
.sidebar-desktop {
display: block;
}
.mobile-header {
display: none;
}
.brand {
font-family: 'Brygada 1918', serif;
font-size: 16px;
font-weight: 600;
color: var(--candle);
text-decoration: none;
}
@media (max-width: 768px) {
.sidebar-desktop {
display: none;
}
.mobile-header {
display: flex;
padding: 12px 20px;
border-bottom: 1px dashed var(--border);
align-items: center;
justify-content: space-between;
background: var(--bg);
position: sticky;
top: 0;
z-index: 50;
}
.main {
margin-left: 0;
}
}
</style>