ghostguild-org/app/layouts/admin.vue
Jennie Robinson Faber 501be10bfe feat: pre-registrant management and invitation system
Admin interface to review, filter, and batch-invite the 95 pre-registrants
from Baby Ghosts. Accept-invitation page pre-fills their data and collects
circle, pronouns, motivation, contribution tier, and agreement before
creating their member record.
2026-04-06 14:46:11 +01:00

330 lines
7.7 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-(--bg) focus:text-(--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/pre-registrants"
:class="{ active: route.path.startsWith('/admin/pre-registrants') }"
>
Pre-Registrants
</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/pre-registrants"
:class="{ active: route.path.startsWith('/admin/pre-registrants') }"
@click="isMobileMenuOpen = false"
>
Pre-Registrants
</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 pageBreadcrumbTitle = useState("pageBreadcrumbTitle", () => "");
const currentPageName = computed(() => {
const path = route.path;
if (path === "/admin") return "admin";
const segments = path.slice(1).split("/");
if (pageBreadcrumbTitle.value && segments.length > 1) {
return [...segments.slice(0, -1), pageBreadcrumbTitle.value].join(" / ");
}
return segments.join(" / ");
});
</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>