aniqove / frontend /src /components /Header.jsx
mwask's picture
Upload 59 files
d571830 verified
Raw
History Blame Contribute Delete
6.89 kB
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>
);
}