elearning/Frontend-Learner/components/user/UserMenu.vue
2026-01-13 10:48:02 +07:00

210 lines
6.2 KiB
Vue

<script setup lang="ts">
/**
* @file UserMenu.vue
* @description User profile dropdown menu component.
* Features:
* - Displays user initials and info
* - Navigation links
* - Dark mode toggle
* - Logout functionality
* - Responsive positioning using Teleport
*/
import { ref, computed, onMounted } from 'vue'
import { useAuth } from '~/composables/useAuth'
const { currentUser, logout } = useAuth()
const isOpen = ref(false)
const isDarkMode = ref(false)
const menuRef = ref<HTMLDivElement | null>(null)
// Sync Dark Mode state on mount
onMounted(() => {
// Should default to light, so we ensure state matches.
if (document.documentElement.classList.contains('dark')) {
isDarkMode.value = true
}
})
// Position calculation for the dropdown
const menuPosition = ref({ top: '0px', right: '0px' })
// Computed property to display user initials (e.g. "JD" for John Doe)
const userInitials = computed(() => {
const f = currentUser.value.firstName?.charAt(0).toUpperCase() || ''
const l = currentUser.value.lastName?.charAt(0).toUpperCase() || ''
return f + l
})
const userName = computed(() => {
return `${currentUser.value.firstName} ${currentUser.value.lastName}`
})
// Navigation menu definition
const menuItems = [
{ label: 'หน้าหลัก', to: '/dashboard' },
{ label: 'รายการคอร์ส', to: '/browse/discovery' },
{ label: 'คอร์สของฉัน', to: '/dashboard/my-courses' },
{ label: 'ตั้งค่าบัญชี', to: '/dashboard/profile' }
]
const handleNavigate = (path: string) => {
navigateTo(path)
isOpen.value = false
}
const handleLogout = async () => {
await logout()
}
/**
* Toggles dark mode on the document element.
*/
const toggleDarkMode = () => {
isDarkMode.value = !isDarkMode.value
if (isDarkMode.value) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
/**
* Calculates and updates the menu position relative to the button.
*/
const updateMenuPosition = () => {
if (menuRef.value) {
const rect = menuRef.value.getBoundingClientRect()
menuPosition.value = {
top: `${rect.bottom + 8}px`,
right: `${window.innerWidth - rect.right}px`
}
}
}
const toggleMenu = () => {
isOpen.value = !isOpen.value
if (isOpen.value) {
updateMenuPosition()
}
}
// Event listeners for closing menu on click outside and resizing
onMounted(() => {
const handleClickOutside = (e: MouseEvent) => {
if (isOpen.value && menuRef.value && !menuRef.value.contains(e.target as Node)) {
isOpen.value = false
}
}
document.addEventListener('click', handleClickOutside)
return () => {
document.removeEventListener('click', handleClickOutside)
}
})
onMounted(() => {
const handleResize = () => {
if (isOpen.value) {
updateMenuPosition()
}
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
})
</script>
<template>
<div ref="menuRef" class="relative">
<!-- Avatar Button -->
<button
class="flex items-center justify-center w-10 h-10 rounded-full bg-blue-500 text-white font-bold text-sm hover:bg-blue-600 transition-colors cursor-pointer ring-2 ring-blue-500/30"
:title="userName"
@click="toggleMenu"
>
{{ userInitials }}
</button>
<!-- Dropdown Menu - Teleport to body to avoid overflow-hidden -->
<Teleport to="body">
<Transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div
v-if="isOpen"
class="fixed rounded-2xl shadow-lg dark:shadow-2xl z-50 w-64 border overflow-hidden transition-colors"
style="background-color: var(--bg-surface); border-color: rgba(255,255,255,0.1);"
:style="{
top: menuPosition.top,
right: menuPosition.right
}"
>
<!-- Menu Items -->
<nav class="py-2">
<button
v-for="item in menuItems"
:key="item.label"
class="w-full px-6 py-3 text-left transition-colors text-sm font-medium hover:bg-slate-100 dark:hover:bg-white/10 text-slate-800 dark:text-white"
@click="handleNavigate(item.to)"
>
{{ item.label }}
</button>
</nav>
<!-- Dark Mode Toggle -->
<div class="px-6 py-3 border-slate-200 dark:border-slate-700">
<button
class="w-full flex items-center justify-between py-2"
@click="toggleDarkMode"
>
<span class="text-sm font-medium text-slate-800 dark:text-white">โหมดกลางค</span>
<div
:class="[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
isDarkMode ? 'bg-blue-600' : 'bg-slate-300'
]"
>
<span
:class="[
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
isDarkMode ? 'translate-x-6' : 'translate-x-1'
]"
/>
</div>
</button>
</div>
<!-- Logout Button -->
<div class="px-6 py-3 border-t border-slate-200 dark:border-slate-700">
<button
class="w-full px-4 py-2 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/40 rounded-lg font-medium transition-colors text-sm"
@click="handleLogout"
>
ออกจากระบบ
</button>
</div>
</div>
</Transition>
</Teleport>
<!-- Backdrop to close menu -->
<div
v-if="isOpen"
class="fixed inset-0 z-40"
@click="isOpen = false"
/>
</div>
</template>
<style scoped>
button {
transition: all 0.2s ease;
}
</style>