AnhviNguyen
fix listening-admin-key
65d3935
Raw
History Blame Contribute Delete
10.6 kB
<template>
<div
v-if="ui.sidebarMobileOpen"
class="fixed inset-0 z-40 bg-black/50 lg:hidden"
aria-hidden="true"
@click="ui.closeMobileSidebar()"
/>
<nav
class="fixed inset-y-0 left-0 z-50 flex w-[220px] flex-col border-r border-[var(--border-button)] bg-[var(--bg-base)] transition-all duration-200 lg:translate-x-0"
:class="[
ui.sidebarMobileOpen ? 'translate-x-0' : '-translate-x-full',
collapsed ? 'lg:w-16' : 'lg:w-[220px]',
]"
>
<!-- Brand -->
<div class="flex h-16 shrink-0 items-center justify-between border-b border-[var(--border-button)] px-4">
<RouterLink
v-if="!collapsed || !ui.isLargeScreen"
to="/"
class="flex min-w-0 items-center gap-2"
@click="ui.closeMobileSidebar()"
>
<span class="text-[15px] font-bold tracking-tight text-[var(--text-base)]">Lingua<span class="text-[var(--spotify-green)]">IELTS</span></span>
</RouterLink>
<button
class="ml-auto flex h-7 w-7 items-center justify-center rounded-full text-[var(--text-subdued)] transition-colors hover:bg-[var(--bg-interactive)] hover:text-[var(--text-base)] lg:hidden"
title="Đóng menu"
@click="ui.closeMobileSidebar()"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<button
class="hidden h-7 w-7 items-center justify-center rounded-full text-[var(--text-subdued)] transition-colors hover:bg-[var(--bg-interactive)] hover:text-[var(--text-base)] lg:ml-auto lg:flex"
:title="collapsed ? 'Mở rộng' : 'Thu nhỏ'"
@click="toggle"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline v-if="collapsed" points="9 18 15 12 9 6"/>
<polyline v-else points="15 18 9 12 15 6"/>
</svg>
</button>
</div>
<!-- Nav -->
<div class="flex-1 overflow-y-auto overflow-x-hidden py-3" :class="collapsed && ui.isLargeScreen ? 'px-2' : 'px-3'">
<template v-for="group in navGroups" :key="group.label">
<p v-if="!collapsed || !ui.isLargeScreen" class="mb-1 mt-4 px-2 text-[9px] font-bold uppercase tracking-widest text-[var(--text-subdued)] first:mt-1 opacity-60">{{ group.label }}</p>
<RouterLink
v-for="item in group.items"
:key="item.to"
:to="item.to"
:title="collapsed && ui.isLargeScreen ? item.label : undefined"
class="mb-0.5 flex items-center rounded-full px-2 py-2 text-[14px] font-normal text-[var(--text-subdued)] transition-all duration-200 hover:bg-[var(--bg-interactive)] hover:text-[var(--text-base)]"
:class="collapsed && ui.isLargeScreen ? 'justify-center' : 'gap-2.5'"
active-class="!bg-[var(--bg-interactive)] !font-bold !text-[var(--text-base)]"
@click="ui.closeMobileSidebar()"
>
<span class="flex h-5 w-5 shrink-0 items-center justify-center" v-html="item.icon"></span>
<span v-if="!collapsed || !ui.isLargeScreen" class="truncate">{{ item.label }}</span>
</RouterLink>
</template>
</div>
<!-- User -->
<div class="shrink-0 border-t border-[var(--border-button)] p-3">
<RouterLink
to="/profile"
class="flex items-center gap-2.5 rounded-full px-1 py-2 transition-colors duration-200 hover:bg-[var(--bg-interactive)]"
:class="collapsed && ui.isLargeScreen ? 'justify-center' : ''"
@click="ui.closeMobileSidebar()"
>
<div class="relative h-8 w-8 shrink-0">
<UserAvatar
:url="auth.profile?.avatar_url"
:alt="userName"
class="h-8 w-8 rounded-full object-cover"
/>
</div>
<div v-if="!collapsed || !ui.isLargeScreen" class="min-w-0">
<div class="truncate text-[12px] font-bold text-[var(--text-base)]">{{ userName }}</div>
<div class="text-[10px] text-[var(--text-subdued)]">{{ streak }} ngày streak</div>
</div>
</RouterLink>
</div>
</nav>
</template>
<script setup>
import { computed } from 'vue'
import { useAuthStore } from '@/stores/auth.js'
import { useUiStore } from '@/stores/ui.js'
import UserAvatar from '@/components/ui/UserAvatar.vue'
const auth = useAuthStore()
const ui = useUiStore()
const collapsed = computed(() => ui.sidebarCollapsed)
function toggle() { ui.toggleSidebar() }
const userName = computed(() => auth.profile?.full_name || auth.profile?.email || 'User')
const streak = computed(() => auth.profile?.streak ?? 0)
const NAV_ICON = {
dashboard: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>`,
reading: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>`,
listening: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/></svg>`,
writing: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
speaking: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>`,
conversation: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`,
mock: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`,
vocab: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`,
shadowing: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/></svg>`,
history: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
leaderboard: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 21H5a2 2 0 0 1-2-2v-5l3-7"/><path d="M16 21h3a2 2 0 0 0 2-2v-5l-3-7"/><path d="M9 7h6m-6 0V3h6v4m-6 0L8 21m7-14 1 14"/></svg>`,
admin: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 4v5c0 5-3 8-7 9-4-1-7-4-7-9V7l7-4z"/><path d="M9 12l2 2 4-4"/></svg>`,
users: `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
}
const navGroups = computed(() => {
const groups = [
{
label: 'Tổng quan',
items: [
{ to: '/dashboard', label: 'Dashboard', icon: NAV_ICON.dashboard },
]
},
{
label: 'Luyện tập',
items: [
{ to: '/reading', label: 'Reading', icon: NAV_ICON.reading },
{ to: '/listening', label: 'Listening', icon: NAV_ICON.listening },
{ to: '/writing', label: 'Writing', icon: NAV_ICON.writing },
{ to: '/speaking', label: 'Speaking', icon: NAV_ICON.speaking },
{ to: '/conversation', label: 'Conversation', icon: NAV_ICON.conversation },
{ to: '/full-exam', label: 'Full Mock', icon: NAV_ICON.mock },
{ to: '/shadowing', label: 'Shadowing', icon: NAV_ICON.shadowing },
]
},
{
label: 'Học tập',
items: [
{ to: '/vocabulary', label: 'Từ vựng', icon: NAV_ICON.vocab },
{ to: '/history', label: 'Lịch sử', icon: NAV_ICON.history },
{ to: '/leaderboard', label: 'Bảng xếp hạng', icon: NAV_ICON.leaderboard },
]
},
]
if (auth.profile?.role === 'admin') {
groups.push({
label: 'Admin',
items: [
{ to: '/admin', label: 'Tổng quan admin', icon: NAV_ICON.admin },
{ to: '/admin/users', label: 'Người dùng', icon: NAV_ICON.users },
{ to: '/admin/leaderboard', label: 'Quản trị BXH', icon: NAV_ICON.leaderboard },
{ to: '/admin/system-vocab', label: 'System vocab', icon: NAV_ICON.vocab },
{ to: '/admin/content/writing', label: 'Writing CMS', icon: NAV_ICON.writing },
{ to: '/admin/content/translation', label: 'Translation CMS', icon: NAV_ICON.writing },
{ to: '/admin/content/conversation', label: 'Conversation CMS', icon: NAV_ICON.conversation },
{ to: '/admin/content/listening', label: 'Listening CMS', icon: NAV_ICON.listening },
{ to: '/admin/content/speaking', label: 'Speaking CMS', icon: NAV_ICON.speaking },
{ to: '/admin/content/mock-tests', label: 'Mock CMS', icon: NAV_ICON.mock },
],
})
}
return groups
})
</script>