212 lines
6.3 KiB
Vue
212 lines
6.3 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(() => {
|
|
if (!currentUser.value) return ''
|
|
const f = currentUser.value.firstName?.charAt(0).toUpperCase() || ''
|
|
const l = currentUser.value.lastName?.charAt(0).toUpperCase() || ''
|
|
return f + l
|
|
})
|
|
|
|
const userName = computed(() => {
|
|
if (!currentUser.value) return ''
|
|
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>
|