2026-09-03 21:01:52 +03:30
|
|
|
import { useEffect, useRef, useState } from 'react';
|
|
|
|
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
|
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
|
|
|
import { APP_NAME } from '@/utils/constants';
|
2026-09-09 01:31:24 +03:30
|
|
|
import { BrandLogo } from '@/components/BrandLogo';
|
2026-09-21 08:00:24 +03:30
|
|
|
import { ClipboardList, LogOut, Moon, NotebookPen, Sun, User } from 'lucide-react';
|
2026-09-03 21:01:52 +03:30
|
|
|
import { MOTION_DURATION, MOTION_EASE, fastTransition } from '@/lib/motion';
|
|
|
|
|
import { useTheme } from '@/hooks/useTheme';
|
2026-09-09 01:31:24 +03:30
|
|
|
import { cn } from '@/lib/utils';
|
2026-09-03 21:01:52 +03:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Minimal top shell: product mark + profile menu (Course Status, Logout).
|
|
|
|
|
* Replaces the former bottom navigation.
|
|
|
|
|
*/
|
|
|
|
|
export function AppHeader() {
|
2026-09-09 01:31:24 +03:30
|
|
|
const { isAuthenticated, signOut, isGuest, user } = useAuth();
|
2026-09-03 21:01:52 +03:30
|
|
|
const { toggleTheme, isDark } = useTheme();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
|
|
|
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
|
|
|
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
const isAuthPage = location.pathname === '/login' || location.pathname === '/signup';
|
2026-09-09 01:31:24 +03:30
|
|
|
const usernameLabel = isGuest
|
|
|
|
|
? 'مهمان'
|
|
|
|
|
: user?.email?.trim() || 'کاربر';
|
2026-09-03 21:01:52 +03:30
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setMenuOpen(false);
|
|
|
|
|
}, [location.pathname]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!menuOpen) return;
|
|
|
|
|
|
|
|
|
|
const onPointerDown = (event: MouseEvent) => {
|
|
|
|
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
|
|
|
|
setMenuOpen(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-09 01:31:24 +03:30
|
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
if (event.key === 'Escape') setMenuOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
2026-09-03 21:01:52 +03:30
|
|
|
document.addEventListener('mousedown', onPointerDown);
|
2026-09-09 01:31:24 +03:30
|
|
|
document.addEventListener('keydown', onKeyDown);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('mousedown', onPointerDown);
|
|
|
|
|
document.removeEventListener('keydown', onKeyDown);
|
|
|
|
|
};
|
2026-09-03 21:01:52 +03:30
|
|
|
}, [menuOpen]);
|
|
|
|
|
|
|
|
|
|
const handleSignOut = async () => {
|
|
|
|
|
await signOut();
|
|
|
|
|
setShowLogoutConfirm(false);
|
|
|
|
|
setMenuOpen(false);
|
|
|
|
|
navigate('/login', { replace: true });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isAuthPage || (!isAuthenticated && !isGuest)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2026-09-09 01:31:24 +03:30
|
|
|
<header className="app-header">
|
2026-09-03 21:01:52 +03:30
|
|
|
<div className="mx-auto flex h-14 w-full items-center justify-between gap-3 px-4">
|
|
|
|
|
<Link
|
|
|
|
|
to="/"
|
2026-09-09 01:31:24 +03:30
|
|
|
className="inline-flex shrink-0 items-center transition-opacity duration-300 hover:opacity-90"
|
2026-09-03 21:01:52 +03:30
|
|
|
aria-label={APP_NAME}
|
|
|
|
|
>
|
2026-09-09 01:31:24 +03:30
|
|
|
<BrandLogo imgClassName="h-7 sm:h-8" />
|
2026-09-03 21:01:52 +03:30
|
|
|
</Link>
|
|
|
|
|
|
|
|
|
|
<div className="relative" ref={menuRef}>
|
|
|
|
|
<motion.button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setMenuOpen((open) => !open)}
|
2026-09-09 01:31:24 +03:30
|
|
|
whileHover={{ scale: 1.04 }}
|
2026-09-03 21:01:52 +03:30
|
|
|
whileTap={{ scale: 0.96 }}
|
|
|
|
|
transition={fastTransition}
|
2026-09-09 01:31:24 +03:30
|
|
|
className={cn(
|
|
|
|
|
'inline-flex h-9 w-9 items-center justify-center rounded-full border transition-colors duration-200',
|
|
|
|
|
'border-slate-200 bg-white text-sky-700 shadow-sm',
|
|
|
|
|
'hover:border-sky-300 hover:bg-sky-50 hover:text-sky-800',
|
|
|
|
|
'dark:border-slate-600/60 dark:bg-slate-800 dark:text-sky-300 dark:shadow-none',
|
|
|
|
|
'dark:hover:border-slate-500 dark:hover:bg-slate-700 dark:hover:text-sky-200',
|
|
|
|
|
menuOpen && 'border-sky-400 ring-2 ring-sky-400/25 dark:border-sky-500 dark:ring-sky-500/20'
|
|
|
|
|
)}
|
2026-09-03 21:01:52 +03:30
|
|
|
aria-label="منوی پروفایل"
|
|
|
|
|
aria-expanded={menuOpen}
|
|
|
|
|
aria-haspopup="menu"
|
|
|
|
|
>
|
|
|
|
|
<User className="h-4 w-4" />
|
|
|
|
|
</motion.button>
|
|
|
|
|
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
{menuOpen && (
|
|
|
|
|
<motion.div
|
2026-09-09 01:31:24 +03:30
|
|
|
key="menu-panel"
|
2026-09-03 21:01:52 +03:30
|
|
|
role="menu"
|
2026-09-09 01:31:24 +03:30
|
|
|
initial={{ opacity: 0, scale: 0.96, y: -4 }}
|
|
|
|
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
|
|
|
exit={{ opacity: 0, scale: 0.96, y: -4 }}
|
2026-09-03 21:01:52 +03:30
|
|
|
transition={{ duration: MOTION_DURATION.fast, ease: MOTION_EASE }}
|
2026-09-09 01:31:24 +03:30
|
|
|
className="header-menu-panel"
|
2026-09-03 21:01:52 +03:30
|
|
|
>
|
2026-09-09 01:31:24 +03:30
|
|
|
<div className="header-menu-identity" aria-label="نام کاربری">
|
|
|
|
|
<p className="text-[11px] font-medium text-muted-foreground">نام کاربری</p>
|
|
|
|
|
<p
|
|
|
|
|
className="truncate text-sm font-semibold text-foreground"
|
|
|
|
|
title={usernameLabel}
|
|
|
|
|
dir="ltr"
|
|
|
|
|
>
|
|
|
|
|
{usernameLabel}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-09-03 21:01:52 +03:30
|
|
|
<Link
|
|
|
|
|
to="/course-status"
|
|
|
|
|
role="menuitem"
|
2026-09-09 01:31:24 +03:30
|
|
|
className="header-menu-item"
|
2026-09-03 21:01:52 +03:30
|
|
|
onClick={() => setMenuOpen(false)}
|
|
|
|
|
>
|
2026-09-09 01:31:24 +03:30
|
|
|
<ClipboardList className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
|
2026-09-03 21:01:52 +03:30
|
|
|
وضعیت دوره
|
|
|
|
|
</Link>
|
2026-09-21 08:00:24 +03:30
|
|
|
<Link
|
|
|
|
|
to="/assignments"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
className="header-menu-item"
|
|
|
|
|
onClick={() => setMenuOpen(false)}
|
|
|
|
|
>
|
|
|
|
|
<NotebookPen className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
|
|
|
|
|
تکالیف من
|
|
|
|
|
</Link>
|
|
|
|
|
{user?.role === 'admin' ? (
|
|
|
|
|
<Link
|
|
|
|
|
to="/admin"
|
|
|
|
|
role="menuitem"
|
|
|
|
|
className="header-menu-item"
|
|
|
|
|
onClick={() => setMenuOpen(false)}
|
|
|
|
|
>
|
|
|
|
|
<ClipboardList className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
|
|
|
|
|
پنل ادمین
|
|
|
|
|
</Link>
|
|
|
|
|
) : null}
|
2026-09-03 21:01:52 +03:30
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
role="menuitem"
|
2026-09-09 01:31:24 +03:30
|
|
|
className="header-menu-item"
|
2026-09-03 21:01:52 +03:30
|
|
|
onClick={toggleTheme}
|
|
|
|
|
>
|
|
|
|
|
{isDark ? (
|
2026-09-09 01:31:24 +03:30
|
|
|
<Sun className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
|
2026-09-03 21:01:52 +03:30
|
|
|
) : (
|
2026-09-09 01:31:24 +03:30
|
|
|
<Moon className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
|
2026-09-03 21:01:52 +03:30
|
|
|
)}
|
|
|
|
|
{isDark ? 'حالت روشن' : 'حالت تاریک'}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
role="menuitem"
|
2026-09-09 01:31:24 +03:30
|
|
|
className="header-menu-item header-menu-item-danger"
|
2026-09-03 21:01:52 +03:30
|
|
|
onClick={() => {
|
|
|
|
|
setMenuOpen(false);
|
|
|
|
|
setShowLogoutConfirm(true);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<LogOut className="h-4 w-4 shrink-0" />
|
|
|
|
|
خروج
|
|
|
|
|
</button>
|
|
|
|
|
</motion.div>
|
|
|
|
|
)}
|
|
|
|
|
</AnimatePresence>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
{showLogoutConfirm && (
|
2026-09-09 01:31:24 +03:30
|
|
|
<div className="fixed inset-0 z-[60] flex items-end justify-center bg-slate-900/50 p-4 backdrop-blur-md sm:items-center">
|
2026-09-03 21:01:52 +03:30
|
|
|
<div className="glass-panel-strong w-full max-w-sm space-y-4 p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-foreground">خروج از حساب کاربری</h3>
|
2026-09-09 01:31:24 +03:30
|
|
|
<p className="text-sm leading-relaxed text-body">آیا مطمئن هستید که میخواهید خارج شوید؟</p>
|
2026-09-03 21:01:52 +03:30
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setShowLogoutConfirm(false)}
|
|
|
|
|
className="btn-glass flex-1"
|
|
|
|
|
>
|
|
|
|
|
انصراف
|
|
|
|
|
</button>
|
|
|
|
|
<button type="button" onClick={handleSignOut} className="btn-danger-neon flex-1">
|
|
|
|
|
خروج
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|