Files
pwa/src/components/AppHeader.tsx
T

205 lines
7.8 KiB
TypeScript
Raw Normal View History

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';
import { BrandLogo } from '@/components/BrandLogo';
import { ClipboardList, LogOut, Moon, NotebookPen, Sun, User } from 'lucide-react';
import { MOTION_DURATION, MOTION_EASE, fastTransition } from '@/lib/motion';
import { useTheme } from '@/hooks/useTheme';
import { cn } from '@/lib/utils';
/**
* Minimal top shell: product mark + profile menu (Course Status, Logout).
* Replaces the former bottom navigation.
*/
export function AppHeader() {
const { isAuthenticated, signOut, isGuest, user } = useAuth();
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';
const usernameLabel = isGuest
? 'مهمان'
: user?.email?.trim() || 'کاربر';
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);
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setMenuOpen(false);
};
document.addEventListener('mousedown', onPointerDown);
document.addEventListener('keydown', onKeyDown);
return () => {
document.removeEventListener('mousedown', onPointerDown);
document.removeEventListener('keydown', onKeyDown);
};
}, [menuOpen]);
const handleSignOut = async () => {
await signOut();
setShowLogoutConfirm(false);
setMenuOpen(false);
navigate('/login', { replace: true });
};
if (isAuthPage || (!isAuthenticated && !isGuest)) {
return null;
}
return (
<>
<header className="app-header">
<div className="mx-auto flex h-14 w-full items-center justify-between gap-3 px-4">
<Link
to="/"
className="inline-flex shrink-0 items-center transition-opacity duration-300 hover:opacity-90"
aria-label={APP_NAME}
>
<BrandLogo imgClassName="h-7 sm:h-8" />
</Link>
<div className="relative" ref={menuRef}>
<motion.button
type="button"
onClick={() => setMenuOpen((open) => !open)}
whileHover={{ scale: 1.04 }}
whileTap={{ scale: 0.96 }}
transition={fastTransition}
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'
)}
aria-label="منوی پروفایل"
aria-expanded={menuOpen}
aria-haspopup="menu"
>
<User className="h-4 w-4" />
</motion.button>
<AnimatePresence>
{menuOpen && (
<motion.div
key="menu-panel"
role="menu"
initial={{ opacity: 0, scale: 0.96, y: -4 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: -4 }}
transition={{ duration: MOTION_DURATION.fast, ease: MOTION_EASE }}
className="header-menu-panel"
>
<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>
<Link
to="/course-status"
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>
<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}
<button
type="button"
role="menuitem"
className="header-menu-item"
onClick={toggleTheme}
>
{isDark ? (
<Sun className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
) : (
<Moon className="h-4 w-4 shrink-0 text-sky-600 dark:text-sky-400" />
)}
{isDark ? 'حالت روشن' : 'حالت تاریک'}
</button>
<button
type="button"
role="menuitem"
className="header-menu-item header-menu-item-danger"
onClick={() => {
setMenuOpen(false);
setShowLogoutConfirm(true);
}}
>
<LogOut className="h-4 w-4 shrink-0" />
خروج
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</header>
{showLogoutConfirm && (
<div className="fixed inset-0 z-[60] flex items-end justify-center bg-slate-900/50 p-4 backdrop-blur-md sm:items-center">
<div className="glass-panel-strong w-full max-w-sm space-y-4 p-6">
<h3 className="text-lg font-semibold text-foreground">خروج از حساب کاربری</h3>
<p className="text-sm leading-relaxed text-body">آیا مطمئن هستید که می‌خواهید خارج شوید؟</p>
<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>
)}
</>
);
}