Files
dating-app-frontend/src/components/layout/SideNav.vue
Oscar 78fe3b5a98 feat(src/composables/useFeed.ts): добавляет поддержку бесконечной прокрутки для ленты новостей
 feat(package.json): добавляет скрипт для генерации спрайта и обновляет зависимости
🔧 refactor(src/composables/useChat.ts): упрощает логику управления чатом, убирает устаревшие функции
🔧 refactor(src/views/chat/ChatRoomView.vue): обновляет логику загрузки сообщений с использованием новых хуков
🔧 refactor(src/views/matches/MatchesView.vue): улучшает обработку совпадений с использованием новых хуков
 feat(src/App.vue): добавляет загрузку справочных данных с использованием Vue Query
 feat(src/main.ts): интегрирует Vue Query в приложение для управления состоянием запросов
 feat(src/views/profile/MyProfileView.vue): добавляет мутацию для удаления профиля с использованием Vue Query
🔧 refactor(src/components/layout/BottomNav.vue): заменяет компонент иконки на более универсальный компонент AppIcon
2026-06-09 10:29:52 +03:00

218 lines
5.0 KiB
Vue

<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">Dating</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">
<AppIcon :name="item.icon" :size="20" />
</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"
:aria-label="uiStore.sidebarExpanded ? 'Свернуть меню' : 'Развернуть меню'"
@click="toggle"
>
<span class="sidenav__icon">
<AppIcon :name="uiStore.sidebarExpanded ? 'chevron-left' : 'chevron-right'" :size="20" />
</span>
</button>
</div>
</nav>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { useAuth } from '@/composables/useAuth'
import { useUi } from '@/composables/useUi'
import AppIcon from '@/components/common/AppIcon.vue'
import type { IconName } from '@/assets/icons/icon-names'
const route = useRoute()
const authStore = useAuth()
const uiStore = useUi()
interface NavItem {
name: string
path: string
label: string
icon: IconName
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>
<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;
}
&__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>