✨ 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
218 lines
5.0 KiB
Vue
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>
|