Files
pwa/src/components/AppHeader.tsx
T

154 lines
6.2 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 { ClipboardList, LogOut, Moon, Sun, User } from 'lucide-react';
import { MOTION_DURATION, MOTION_EASE, fastTransition } from '@/lib/motion';
import { useTheme } from '@/hooks/useTheme';
/**
* Minimal top shell: product mark + profile menu (Course Status, Logout).
* Replaces the former bottom navigation.
*/
export function AppHeader() {
const { isAuthenticated, signOut, isGuest } = 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';
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);
}
};
document.addEventListener('mousedown', onPointerDown);
return () => document.removeEventListener('mousedown', onPointerDown);
}, [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 border-b border-border/40 backdrop-blur-2xl">
<div className="mx-auto flex h-14 w-full items-center justify-between gap-3 px-4">
<Link
to="/"
className="truncate text-base font-semibold tracking-tight text-foreground"
aria-label={APP_NAME}
>
{APP_NAME}
</Link>
<div className="relative" ref={menuRef}>
<motion.button
type="button"
onClick={() => setMenuOpen((open) => !open)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.96 }}
transition={fastTransition}
className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-cyan-200 bg-white text-cyan-700 shadow-sm transition-colors hover:border-cyan-500 hover:text-cyan-800 dark:border-neon-cyan/40 dark:bg-background/70 dark:text-neon-cyan dark:shadow-none dark:hover:border-neon-cyan dark:hover:text-neon-cyan dark:hover:shadow-glow-sm"
aria-label="منوی پروفایل"
aria-expanded={menuOpen}
aria-haspopup="menu"
>
<User className="h-4 w-4" />
</motion.button>
<AnimatePresence>
{menuOpen && (
<motion.div
role="menu"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
transition={{ duration: MOTION_DURATION.fast, ease: MOTION_EASE }}
className="glass-panel-strong absolute left-0 z-[70] mt-2 w-52 origin-top-left p-1"
>
<Link
to="/course-status"
role="menuitem"
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-body transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/5 dark:hover:text-white"
onClick={() => setMenuOpen(false)}
>
<ClipboardList className="h-4 w-4 shrink-0 text-neon-cyan" />
وضعیت دوره
</Link>
<button
type="button"
role="menuitem"
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-body transition-colors hover:bg-black/5 hover:text-foreground dark:hover:bg-white/5 dark:hover:text-white"
onClick={toggleTheme}
>
{isDark ? (
<Sun className="h-4 w-4 shrink-0 text-neon-cyan" />
) : (
<Moon className="h-4 w-4 shrink-0 text-neon-cyan" />
)}
{isDark ? 'حالت روشن' : 'حالت تاریک'}
</button>
<button
type="button"
role="menuitem"
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-600 transition-colors hover:bg-red-500/10 dark:text-red-300"
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-navy-deep/70 p-4 backdrop-blur-sm 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 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>
)}
</>
);
}