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.
260 lines
6.3 KiB
Vue
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>
|