Spaces:
Build error
Build error
| import { useEffect, useState } from "react"; | |
| import { Link, useLocation, useNavigate } from "react-router-dom"; | |
| import { api } from "../api.js"; | |
| import { clearSession, getToken, getUser, setSession } from "../utils/auth.js"; | |
| import AuthModal from "./AuthModal.jsx"; | |
| const NAV = [ | |
| { to: "/home", label: "Home" }, | |
| { to: "/browse", label: "Browse" }, | |
| { to: "/schedule", label: "Schedule" }, | |
| { to: "/movies", label: "Movies & TV" }, | |
| { to: "/manga", label: "Manga" }, | |
| { to: "/music", label: "Music" }, | |
| ]; | |
| export default function Header() { | |
| const [scrolled, setScrolled] = useState(false); | |
| const [q, setQ] = useState(""); | |
| const [user, setUser] = useState(() => getUser()); | |
| const [menuOpen, setMenuOpen] = useState(false); | |
| const [modalOpen, setModalOpen] = useState(false); | |
| const location = useLocation(); | |
| const navigate = useNavigate(); | |
| useEffect(() => { | |
| const fn = () => setScrolled(window.scrollY > 8); | |
| window.addEventListener("scroll", fn, { passive: true }); | |
| return () => window.removeEventListener("scroll", fn); | |
| }, []); | |
| // Hydrate a stored token into the full user profile on load. | |
| useEffect(() => { | |
| if (getToken() && !getUser()) { | |
| api | |
| .authMe() | |
| .then((u) => setSession(getToken(), u)) | |
| .then(() => setUser(getUser())) | |
| .catch(() => clearSession()); | |
| } | |
| }, []); | |
| // Close the dropdown when clicking outside / navigating. | |
| useEffect(() => setMenuOpen(false), [location.pathname]); | |
| useEffect(() => { | |
| if (!menuOpen) return; | |
| const close = (e) => { | |
| if (!e.target.closest?.(".auth-nav")) setMenuOpen(false); | |
| }; | |
| document.addEventListener("click", close); | |
| return () => document.removeEventListener("click", close); | |
| }, [menuOpen]); | |
| useEffect(() => { | |
| const openAuth = () => { | |
| setMenuOpen(false); | |
| setModalOpen(true); | |
| }; | |
| window.addEventListener("open-auth-modal", openAuth); | |
| return () => window.removeEventListener("open-auth-modal", openAuth); | |
| }, []); | |
| function handleSearch(e) { | |
| e.preventDefault(); | |
| if (q.trim()) navigate(`/search?q=${encodeURIComponent(q.trim())}`); | |
| } | |
| async function handleLogout() { | |
| try { | |
| await api.authLogout(); | |
| } catch { | |
| /* token already invalid — clear locally anyway */ | |
| } | |
| clearSession(); | |
| setUser(null); | |
| setMenuOpen(false); | |
| } | |
| return ( | |
| <header id="header" className={scrolled ? "scrolled" : ""}> | |
| <div className="container"> | |
| <Link to="/home" className="logo"> | |
| <div className="logo-wordmark"> | |
| {"ANICOVE".split("").map((ch, i) => ( | |
| <span key={i} className="l">{ch}</span> | |
| ))} | |
| </div> | |
| </Link> | |
| <nav id="main-nav"> | |
| {NAV.map(({ to, label }) => ( | |
| <Link | |
| key={to} | |
| to={to} | |
| className={location.pathname === to ? "active" : ""} | |
| > | |
| {label} | |
| </Link> | |
| ))} | |
| </nav> | |
| <div className="search-wrap"> | |
| <span className="search-icon"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"> | |
| <circle cx="11" cy="11" r="8" /> | |
| <line x1="21" y1="21" x2="16.65" y2="16.65" /> | |
| </svg> | |
| </span> | |
| <form onSubmit={handleSearch}> | |
| <input | |
| id="header-search" | |
| type="text" | |
| value={q} | |
| onChange={e => setQ(e.target.value)} | |
| className="search-input" | |
| placeholder="Search anime..." | |
| autoComplete="off" | |
| /> | |
| </form> | |
| </div> | |
| <div className="auth-nav"> | |
| <button | |
| className={`auth-avatar-btn${user ? "" : " guest"}${menuOpen ? " open" : ""}`} | |
| onClick={() => setMenuOpen((v) => !v)} | |
| aria-label={user ? `${user.username} menu` : "Account menu"} | |
| aria-expanded={menuOpen} | |
| > | |
| {user?.avatar ? ( | |
| <img className="auth-avatar-circle auth-avatar-img" src={user.avatar} alt="" /> | |
| ) : ( | |
| <div className="auth-avatar-circle auth-avatar-icon"> | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"> | |
| <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /> | |
| <circle cx="12" cy="7" r="4" /> | |
| </svg> | |
| </div> | |
| )} | |
| <span>{user ? user.username : "Guest"}</span> | |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> | |
| <path d="M6 9l6 6 6-6" /> | |
| </svg> | |
| </button> | |
| {menuOpen ? ( | |
| <div className="auth-dropdown"> | |
| {user ? ( | |
| <> | |
| <div className="auth-dropdown-head"> | |
| <div className="auth-dropdown-name">{user.username}</div> | |
| <div className="auth-dropdown-sub">{user.email || user.provider}</div> | |
| </div> | |
| <button | |
| className="auth-dropdown-item" | |
| onClick={() => { setMenuOpen(false); navigate("/home"); }} | |
| > | |
| Home | |
| </button> | |
| <button className="auth-dropdown-item danger" onClick={handleLogout}> | |
| Sign out | |
| </button> | |
| </> | |
| ) : ( | |
| <> | |
| <button | |
| className="auth-dropdown-item" | |
| onClick={() => { setMenuOpen(false); setModalOpen(true); }} | |
| > | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"> | |
| <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" /> | |
| <path d="M10 17l5-5-5-5M15 12H3" /> | |
| </svg> | |
| Sign in | |
| </button> | |
| <button | |
| className="auth-dropdown-item" | |
| onClick={() => { window.location.href = "/api/auth/anilist/login"; }} | |
| > | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"> | |
| <path d="M2 3h20v18H2V3zm5.5 5.5h-1.2v7h1.2v-7zm2.6 0h1.2l2.1 5.1 2.1-5.1h1.2v7h-1.15v-4.6L12 15.6l-1.75-4.25v4.15H9.1v-7z" /> | |
| </svg> | |
| Sign in with AniList | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| ) : null} | |
| </div> | |
| <AuthModal | |
| open={modalOpen} | |
| onClose={() => setModalOpen(false)} | |
| onAuthed={(u) => setUser(u)} | |
| /> | |
| </div> | |
| </header> | |
| ); | |
| } | |