213 lines
12 KiB
Vue
213 lines
12 KiB
Vue
<script setup>
|
|
import { useAuthStore } from "~/stores/auth-store.js";
|
|
|
|
const authStore = useAuthStore();
|
|
const router = useRouter();
|
|
|
|
// State (No more isScrolled)
|
|
const isDark = ref(false);
|
|
const isUserMenuOpen = ref(false);
|
|
const isMobileMenuOpen = ref(false);
|
|
const userMenuRef = ref(null);
|
|
|
|
// Toggle Dark Mode
|
|
function toggleDark() {
|
|
isDark.value = !isDark.value;
|
|
if (isDark.value) {
|
|
document.documentElement.classList.add('dark');
|
|
localStorage.theme = 'dark';
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
localStorage.theme = 'light';
|
|
}
|
|
}
|
|
|
|
// Navigation Links
|
|
const navLinks = [
|
|
{ name: 'Dashboard', path: '/' },
|
|
{ name: 'Courses', path: '/courses' },
|
|
{ name: 'Students', path: '/students' },
|
|
{ name: 'Schedule', path: '/schedule' },
|
|
];
|
|
|
|
function handleLogout() {
|
|
isUserMenuOpen.value = false;
|
|
isMobileMenuOpen.value = false;
|
|
authStore.logout();
|
|
router.push("/auth/login");
|
|
}
|
|
|
|
function navigateTo(path) {
|
|
isUserMenuOpen.value = false;
|
|
isMobileMenuOpen.value = false;
|
|
router.push(path);
|
|
}
|
|
|
|
function closeDropdowns(event) {
|
|
if (userMenuRef.value && !userMenuRef.value.contains(event.target)) {
|
|
isUserMenuOpen.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
// Initialize Theme
|
|
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
isDark.value = true;
|
|
document.documentElement.classList.add('dark');
|
|
}
|
|
|
|
// Click listener for closing dropdowns
|
|
window.addEventListener('click', closeDropdowns);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('click', closeDropdowns);
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<nav
|
|
class="sticky top-0 z-50 w-full border-b border-slate-200 dark:border-slate-800 bg-white/95 dark:bg-slate-900/95 backdrop-blur-md transition-colors duration-300">
|
|
<div class="w-full px-6 md:px-8">
|
|
<div class="flex items-center justify-between h-16">
|
|
|
|
<nuxt-link to="/" class="flex items-center gap-2 group flex-shrink-0">
|
|
<div
|
|
class="w-9 h-9 rounded-lg bg-blue-600 flex items-center justify-center text-white font-bold text-xl shadow-blue-500/20 shadow-lg group-hover:scale-110 transition-transform">
|
|
A
|
|
</div>
|
|
<span class="text-xl font-bold text-slate-900 dark:text-white tracking-tight hidden md:block">
|
|
Academic<span class="text-blue-600">Sys</span>
|
|
</span>
|
|
</nuxt-link>
|
|
|
|
<div class="flex items-center gap-4 md:gap-8">
|
|
|
|
<div class="hidden md:flex items-center gap-1">
|
|
<nuxt-link v-for="link in navLinks" :key="link.path" :to="link.path"
|
|
class="px-3 py-2 rounded-md text-sm font-medium text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-slate-50 dark:hover:bg-slate-800 transition-all duration-200"
|
|
active-class="bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-semibold">
|
|
{{ link.name }}
|
|
</nuxt-link>
|
|
</div>
|
|
|
|
<div class="hidden md:block h-6 w-px bg-slate-200 dark:bg-slate-700 mx-2"></div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
<button @click="toggleDark"
|
|
class="p-2.5 rounded-full text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
<svg v-if="!isDark" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
<svg v-else class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div v-if="authStore?.token" class="relative" ref="userMenuRef">
|
|
<button @click.stop="isUserMenuOpen = !isUserMenuOpen"
|
|
class="flex items-center gap-3 pl-2 pr-1 py-1.5 rounded-full border border-transparent hover:bg-slate-100 dark:hover:bg-slate-800 transition-all focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
:class="{ 'bg-slate-100 dark:bg-slate-800': isUserMenuOpen }">
|
|
<span
|
|
class="text-sm font-semibold text-slate-700 dark:text-slate-200 hidden sm:block ml-2">
|
|
{{ authStore.user.name.split(' ')[0] }}
|
|
</span>
|
|
<img :src="authStore.user.avatarUrl || `https://ui-avatars.com/api/?name=${authStore.user.name}&background=0D8ABC&color=fff`"
|
|
alt="Avatar"
|
|
class="w-9 h-9 rounded-full border border-slate-200 dark:border-slate-700 object-cover">
|
|
<svg class="w-4 h-4 text-slate-400" :class="{ 'rotate-180': isUserMenuOpen }" fill="none"
|
|
viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
<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="isUserMenuOpen"
|
|
class="absolute right-0 mt-2 w-64 bg-white dark:bg-slate-900 rounded-xl shadow-xl border border-slate-100 dark:border-slate-800 py-2 focus:outline-none z-50">
|
|
<div class="px-4 py-3 border-b border-slate-100 dark:border-slate-800 mb-1">
|
|
<p class="text-xs text-slate-500 dark:text-slate-400 font-medium">Signed in as
|
|
</p>
|
|
<p class="text-sm font-bold text-slate-900 dark:text-white truncate">{{
|
|
authStore.user.email }}</p>
|
|
</div>
|
|
<button @click="navigateTo('/profile')"
|
|
class="w-full text-left px-4 py-2.5 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors">
|
|
<span>👤</span> Your Profile
|
|
</button>
|
|
<button @click="navigateTo('/settings')"
|
|
class="w-full text-left px-4 py-2.5 text-sm text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-800 flex items-center gap-3 transition-colors">
|
|
<span>⚙️</span> Settings
|
|
</button>
|
|
<div class="border-t border-slate-100 dark:border-slate-800 mt-1 pt-1">
|
|
<button @click="handleLogout"
|
|
class="w-full text-left px-4 py-2.5 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-3 transition-colors">
|
|
<span>🚪</span> Sign out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
|
|
<template v-else>
|
|
<nuxt-link to="/auth/login"
|
|
class="hidden sm:inline-flex items-center justify-center px-6 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm shadow-blue-500/30 transition-all">
|
|
Sign In
|
|
</nuxt-link>
|
|
</template>
|
|
|
|
<button @click="isMobileMenuOpen = !isMobileMenuOpen"
|
|
class="md:hidden p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
|
|
<svg v-if="!isMobileMenuOpen" class="w-7 h-7" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
<svg v-else class="w-7 h-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<transition enter-active-class="transition duration-200 ease-out"
|
|
enter-from-class="transform -translate-y-4 opacity-0" enter-to-class="transform translate-y-0 opacity-100"
|
|
leave-active-class="transition duration-150 ease-in" leave-from-class="transform translate-y-0 opacity-100"
|
|
leave-to-class="transform -translate-y-4 opacity-0">
|
|
<div v-if="isMobileMenuOpen"
|
|
class="md:hidden border-t border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-xl">
|
|
<div class="px-4 pt-2 pb-6 space-y-2">
|
|
<nuxt-link v-for="link in navLinks" :key="link.path" :to="link.path"
|
|
class="block px-3 py-3 rounded-lg text-base font-medium text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-blue-600"
|
|
active-class="bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"
|
|
@click="isMobileMenuOpen = false">
|
|
{{ link.name }}
|
|
</nuxt-link>
|
|
<template v-if="!authStore?.token">
|
|
<nuxt-link to="/auth/login"
|
|
class="block w-full text-center mt-6 px-5 py-3 text-base font-bold text-white bg-blue-600 rounded-lg shadow-lg shadow-blue-500/30">
|
|
Sign In
|
|
</nuxt-link>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</transition>
|
|
</nav>
|
|
|
|
<main class="w-full min-h-screen">
|
|
<slot />
|
|
</main>
|
|
</template>
|