Files
dating-app-frontend/src/components/layout/SideNav.vue
2026-06-08 13:23:20 +03:00

236 lines
6.1 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth.store';
import { useUiStore } from '@/stores/ui.store';
const route = useRoute();
const authStore = useAuthStore();
const uiStore = useUiStore();
interface NavItem {
name: string;
path: string;
label: string;
icon: string;
adminOnly?: boolean;
}
const navItems: NavItem[] = [
{ name: 'feed', path: '/feed', label: 'Лента', icon: 'grid' },
{ name: 'matches', path: '/matches', label: 'Совпадения', icon: 'heart' },
{ name: 'chats', path: '/chats', label: 'Чаты', icon: 'chat' },
{ name: 'dates', path: '/dates', label: 'Встречи', icon: 'calendar' },
{ name: 'profile', path: '/profile/me', label: 'Профиль', icon: 'person' },
];
const adminItems: NavItem[] = [
{ name: 'admin', path: '/admin/reports', label: 'Жалобы', icon: 'flag', adminOnly: true },
];
const visibleItems = computed(() =>
authStore.isAdmin ? [...navItems, ...adminItems] : navItems,
);
function isActive(path: string) {
return route.path.startsWith(path) && path !== '/';
}
function toggle() {
uiStore.setSidebarExpanded(!uiStore.sidebarExpanded);
}
</script>
<template>
<nav
class="sidenav"
:class="{ 'sidenav--expanded': uiStore.sidebarExpanded }"
aria-label="Навигация"
>
<div class="sidenav__header">
<span class="sidenav__logo">D</span>
<Transition name="fade">
<span v-if="uiStore.sidebarExpanded" class="sidenav__brand">Daiting</span>
</Transition>
</div>
<ul class="sidenav__list" role="list">
<li v-for="item in visibleItems" :key="item.path">
<RouterLink
:to="item.path"
class="sidenav__item"
:class="{ 'sidenav__item--active': isActive(item.path) }"
:aria-label="item.label"
:aria-current="isActive(item.path) ? 'page' : undefined"
>
<span class="sidenav__icon" :aria-hidden="true">
<NavIcon :name="item.icon" />
</span>
<Transition name="fade">
<span v-if="uiStore.sidebarExpanded" class="sidenav__label">{{ item.label }}</span>
</Transition>
</RouterLink>
</li>
</ul>
<div class="sidenav__footer">
<button
class="sidenav__toggle"
@click="toggle"
:aria-label="uiStore.sidebarExpanded ? 'Свернуть меню' : 'Развернуть меню'"
>
<span class="sidenav__icon">
<NavIcon :name="uiStore.sidebarExpanded ? 'chevron-left' : 'chevron-right'" />
</span>
</button>
</div>
</nav>
</template>
<script lang="ts">
// Inline icon renderer to avoid external icon library dependency
const NavIcon = {
props: { name: String },
template: `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path v-if="name==='grid'" d="M3 3h7v7H3zm11 0h7v7h-7zM3 14h7v7H3zm11 0h7v7h-7z"/>
<path v-if="name==='heart'" d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
<path v-if="name==='chat'" d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
<path v-if="name==='calendar'" d="M8 2v4M16 2v4M3 10h18M3 6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<path v-if="name==='person'" d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/>
<path v-if="name==='flag'" d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1zM4 22v-7"/>
<path v-if="name==='chevron-right'" d="M9 18l6-6-6-6"/>
<path v-if="name==='chevron-left'" d="M15 18l-6-6 6-6"/>
</svg>
`,
};
</script>
<style scoped lang="scss">
.sidenav {
width: var(--sidebar-collapsed);
height: 100%;
background: var(--color-surface);
border-right: 1px solid var(--color-border);
display: flex;
flex-direction: column;
transition: width var(--transition-base);
overflow: hidden;
flex-shrink: 0;
&--expanded {
width: var(--sidebar-expanded);
}
&__header {
height: 64px;
display: flex;
align-items: center;
padding: 0 18px;
gap: 12px;
border-bottom: 1px solid var(--color-border);
flex-shrink: 0;
}
&__logo {
font-family: var(--font-display);
font-size: 1.5rem;
color: var(--color-signal);
flex-shrink: 0;
width: 28px;
text-align: center;
}
&__brand {
font-family: var(--font-display);
font-size: 1.125rem;
color: var(--color-cream);
white-space: nowrap;
}
&__list {
flex: 1;
padding: 12px 0;
margin: 0;
list-style: none;
overflow-y: auto;
overflow-x: hidden;
}
&__item {
display: flex;
align-items: center;
gap: 12px;
height: 48px;
padding: 0 18px;
color: var(--color-muted);
text-decoration: none;
transition: color var(--transition-fast), background var(--transition-fast);
white-space: nowrap;
&:hover {
color: var(--color-cream);
background: rgba(240, 235, 224, 0.04);
}
&--active {
color: var(--color-cream);
background: rgba(240, 235, 224, 0.06);
.sidenav__icon {
color: var(--color-signal);
}
}
}
&__icon {
width: 20px;
height: 20px;
flex-shrink: 0;
svg {
width: 100%;
height: 100%;
}
}
&__label {
font-family: var(--font-mono);
font-size: 0.8125rem;
font-weight: 400;
letter-spacing: 0.02em;
}
&__footer {
padding: 12px 0;
border-top: 1px solid var(--color-border);
flex-shrink: 0;
}
&__toggle {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
height: 48px;
padding: 0 18px;
background: none;
border: none;
color: var(--color-muted);
cursor: pointer;
transition: color var(--transition-fast);
&:hover { color: var(--color-cream); }
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity var(--transition-fast);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>