Wrap auth-dependent sidebar navigation and meta in ClientOnly with SSR fallback slots to prevent hydration mismatch that caused all authenticated nav links to point to wrong pages. Fix admin events page crash by replacing empty string USelect values with 'all'.
304 lines
7.4 KiB
Vue
304 lines
7.4 KiB
Vue
<template>
|
|
<aside :class="isMobile ? 'sidebar sidebar-mobile' : 'sidebar'">
|
|
<!-- Brand -->
|
|
<NuxtLink to="/" class="sidebar-brand" @click="handleNavigate">
|
|
Ghost Guild
|
|
</NuxtLink>
|
|
|
|
<!-- Navigation sections -->
|
|
<div class="sidebar-body">
|
|
<ClientOnly>
|
|
<template v-if="isAuthenticated">
|
|
<!-- Logged-in nav -->
|
|
<div class="sidebar-section">You</div>
|
|
<ul class="sidebar-nav">
|
|
<li v-for="item in youItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
:class="{ active: isActive(item.path) }"
|
|
@click="handleNavigate"
|
|
>{{ item.label }}</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="sidebar-section">Explore</div>
|
|
<ul class="sidebar-nav">
|
|
<li v-for="item in exploreItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
:class="{ active: isActive(item.path) }"
|
|
@click="handleNavigate"
|
|
>{{ item.label }}</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="sidebar-section">Community</div>
|
|
<ul class="sidebar-nav">
|
|
<li v-for="item in communityItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
:class="{ active: isActive(item.path) }"
|
|
@click="handleNavigate"
|
|
>{{ item.label }}</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<!-- Public nav -->
|
|
<div class="sidebar-section">Navigate</div>
|
|
<ul class="sidebar-nav">
|
|
<li v-for="item in publicItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
:class="{ active: isActive(item.path) }"
|
|
@click="handleNavigate"
|
|
>{{ item.label }}</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="sidebar-section">Join</div>
|
|
<ul class="sidebar-nav">
|
|
<li v-for="item in joinItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
:class="{ active: isActive(item.path) }"
|
|
@click="handleNavigate"
|
|
>{{ item.label }}</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
|
|
<template #fallback>
|
|
<!-- Public nav (SSR fallback) -->
|
|
<div class="sidebar-section">Navigate</div>
|
|
<ul class="sidebar-nav">
|
|
<li v-for="item in publicItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
:class="{ active: isActive(item.path) }"
|
|
@click="handleNavigate"
|
|
>{{ item.label }}</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="sidebar-section">Join</div>
|
|
<ul class="sidebar-nav">
|
|
<li v-for="item in joinItems" :key="item.path">
|
|
<NuxtLink
|
|
:to="item.path"
|
|
:class="{ active: isActive(item.path) }"
|
|
@click="handleNavigate"
|
|
>{{ item.label }}</NuxtLink>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</ClientOnly>
|
|
</div>
|
|
|
|
<!-- Meta at bottom -->
|
|
<div class="sidebar-meta">
|
|
<ClientOnly>
|
|
<template v-if="isAuthenticated">
|
|
<span class="member-name">{{ memberData?.name || 'Member' }}</span><br>
|
|
<span
|
|
v-if="memberData?.circle"
|
|
class="member-circle"
|
|
:style="{ color: `var(--c-${memberData.circle})` }"
|
|
>{{ memberData.circle }}</span>
|
|
<br v-if="memberData?.circle">
|
|
<a href="#" @click.prevent="handleLogout">Sign out</a>
|
|
</template>
|
|
<template v-else>
|
|
Part of <a href="https://babyghosts.fund" target="_blank">Baby Ghosts</a><br>
|
|
A Canadian nonprofit<br>
|
|
<a href="#" @click.prevent="openLogin">Sign in</a>
|
|
</template>
|
|
|
|
<template #fallback>
|
|
Part of <a href="https://babyghosts.fund" target="_blank">Baby Ghosts</a><br>
|
|
A Canadian nonprofit<br>
|
|
<a href="#" @click.prevent="openLogin">Sign in</a>
|
|
</template>
|
|
</ClientOnly>
|
|
<ClientOnly>
|
|
<ColorModeToggle />
|
|
</ClientOnly>
|
|
</div>
|
|
</aside>
|
|
</template>
|
|
|
|
<script setup>
|
|
const props = defineProps({
|
|
isMobile: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['navigate'])
|
|
|
|
const route = useRoute()
|
|
const { isAuthenticated, logout, memberData } = useAuth()
|
|
const { openLoginModal } = useLoginModal()
|
|
|
|
const handleNavigate = () => {
|
|
if (props.isMobile) {
|
|
emit('navigate')
|
|
}
|
|
}
|
|
|
|
const handleLogout = async () => {
|
|
await logout()
|
|
handleNavigate()
|
|
}
|
|
|
|
const openLogin = () => {
|
|
openLoginModal()
|
|
handleNavigate()
|
|
}
|
|
|
|
const isActive = (path) => {
|
|
if (path === '/') return route.path === '/'
|
|
return route.path.startsWith(path)
|
|
}
|
|
|
|
// Public nav items
|
|
const publicItems = [
|
|
{ label: 'Home', path: '/' },
|
|
{ label: 'About', path: '/about' },
|
|
{ label: 'Events', path: '/events' },
|
|
{ label: 'Members', path: '/members' },
|
|
{ label: 'Wiki', path: '/wiki' },
|
|
]
|
|
|
|
const joinItems = [
|
|
{ label: 'Become a member', path: '/join' },
|
|
{ label: 'Propose an event', path: '/events' },
|
|
]
|
|
|
|
// Logged-in nav items
|
|
const youItems = [
|
|
{ label: 'Dashboard', path: '/member/dashboard' },
|
|
{ label: 'Profile', path: '/member/profile' },
|
|
{ label: 'Account', path: '/member/account' },
|
|
{ label: 'My Updates', path: '/member/my-updates' },
|
|
]
|
|
|
|
const exploreItems = [
|
|
{ label: 'Events', path: '/events' },
|
|
{ label: 'Members', path: '/members' },
|
|
{ label: 'Wiki', path: '/wiki' },
|
|
{ label: 'About', path: '/about' },
|
|
]
|
|
|
|
const communityItems = [
|
|
{ label: 'Peer Support', path: '/members' },
|
|
{ label: 'Propose an Event', path: '/events' },
|
|
]
|
|
</script>
|
|
|
|
<style scoped>
|
|
.sidebar {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 220px;
|
|
padding: 0;
|
|
border-right: 1px dashed var(--border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--bg);
|
|
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);
|
|
}
|
|
|
|
.member-name {
|
|
color: var(--text);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.member-circle {
|
|
font-size: 10px;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
</style>
|