avatar menu closes properly

This commit is contained in:
jackiettran
2025-12-30 23:25:50 -05:00
parent 1b4e86be29
commit 3ff98fbe1e

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef, useCallback } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate, useLocation } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { useSocket } from "../contexts/SocketContext"; import { useSocket } from "../contexts/SocketContext";
import { rentalAPI, messageAPI } from "../services/api"; import { rentalAPI, messageAPI } from "../services/api";
@@ -9,9 +9,55 @@ const Navbar: React.FC = () => {
const { user, logout, openAuthModal } = useAuth(); const { user, logout, openAuthModal } = useAuth();
const { onNewMessage, onMessageRead } = useSocket(); const { onNewMessage, onMessageRead } = useSocket();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [pendingRequestsCount, setPendingRequestsCount] = useState(0); const [pendingRequestsCount, setPendingRequestsCount] = useState(0);
const [unreadMessagesCount, setUnreadMessagesCount] = useState(0); const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef<HTMLLIElement>(null);
const mobileMenuRef = useRef<HTMLDivElement>(null);
const mobileToggleRef = useRef<HTMLButtonElement>(null);
const closeDropdown = useCallback(() => {
setIsDropdownOpen(false);
}, []);
const closeMobileMenu = useCallback(() => {
setIsMobileMenuOpen(false);
}, []);
// Close menus when route changes
useEffect(() => {
closeDropdown();
closeMobileMenu();
}, [location.pathname, closeDropdown, closeMobileMenu]);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// Desktop dropdown
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownOpen(false);
}
// Mobile menu
if (
isMobileMenuOpen &&
mobileMenuRef.current &&
mobileToggleRef.current &&
!mobileMenuRef.current.contains(event.target as Node) &&
!mobileToggleRef.current.contains(event.target as Node)
) {
setIsMobileMenuOpen(false);
}
};
document.addEventListener("click", handleClickOutside, true);
return () => {
document.removeEventListener("click", handleClickOutside, true);
};
}, [isMobileMenuOpen]);
// Fetch pending rental requests count when user logs in // Fetch pending rental requests count when user logs in
useEffect(() => { useEffect(() => {
@@ -109,31 +155,16 @@ const Navbar: React.FC = () => {
<> <>
<style> <style>
{` {`
@media (max-width: 991.98px) { #navbarNav .mobile-menu {
#navbarNav { padding: 0;
position: absolute; }
top: calc(100% - 8px); #navbarNav .dropdown-item {
right: 0.5rem; padding: 0.25rem 1rem;
left: auto; }
z-index: 1000; #navbarNav .dropdown-divider {
min-width: 10rem; margin: 0.5rem 0;
padding: 0; border-top: 1px solid rgba(0,0,0,.15);
background-color: #fff; opacity: 1;
border: 1px solid rgba(0,0,0,.15);
border-radius: 0.375rem;
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.175);
}
#navbarNav .mobile-menu {
padding: 0;
}
#navbarNav .dropdown-item {
padding: 0.25rem 1rem;
}
#navbarNav .dropdown-divider {
margin: 0.5rem 0;
border-top: 1px solid rgba(0,0,0,.15);
opacity: 1;
}
} }
`} `}
</style> </style>
@@ -170,13 +201,16 @@ const Navbar: React.FC = () => {
{/* Mobile menu toggle */} {/* Mobile menu toggle */}
<button <button
className="navbar-toggler border-0 p-0" ref={mobileToggleRef}
className="navbar-toggler border-0 p-0 d-lg-none"
type="button" type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-controls="navbarNav"
aria-expanded="false" aria-expanded={isMobileMenuOpen}
aria-label="Toggle navigation" aria-label="Toggle navigation"
onClick={(e) => {
e.stopPropagation();
setIsMobileMenuOpen((prev) => !prev);
}}
> >
{user ? ( {user ? (
<span <span
@@ -219,7 +253,25 @@ const Navbar: React.FC = () => {
></i> ></i>
)} )}
</button> </button>
<div className="collapse navbar-collapse" id="navbarNav"> <div
ref={mobileMenuRef}
id="navbarNav"
className="d-lg-none"
style={{
display: isMobileMenuOpen ? "block" : "none",
position: "absolute",
top: "calc(100% - 8px)",
right: "0.5rem",
left: "auto",
zIndex: 1000,
minWidth: "10rem",
padding: 0,
backgroundColor: "#fff",
border: "1px solid rgba(0,0,0,.15)",
borderRadius: "0.375rem",
boxShadow: "0 0.5rem 1rem rgba(0,0,0,.175)",
}}
>
<div <div
className="d-flex align-items-center justify-content-center justify-content-lg-end w-100 py-3 py-lg-0 mobile-menu" className="d-flex align-items-center justify-content-center justify-content-lg-end w-100 py-3 py-lg-0 mobile-menu"
> >
@@ -228,17 +280,17 @@ const Navbar: React.FC = () => {
<> <>
{/* Mobile menu - show items directly */} {/* Mobile menu - show items directly */}
<li className="d-lg-none"> <li className="d-lg-none">
<Link className="dropdown-item" to="/profile"> <Link className="dropdown-item" to="/profile" onClick={closeMobileMenu}>
<i className="bi bi-person me-2"></i>Profile <i className="bi bi-person me-2"></i>Profile
</Link> </Link>
</li> </li>
<li className="d-lg-none"> <li className="d-lg-none">
<Link className="dropdown-item" to="/renting"> <Link className="dropdown-item" to="/renting" onClick={closeMobileMenu}>
<i className="bi bi-calendar-check me-2"></i>Renting <i className="bi bi-calendar-check me-2"></i>Renting
</Link> </Link>
</li> </li>
<li className="d-lg-none"> <li className="d-lg-none">
<Link className="dropdown-item" to="/owning"> <Link className="dropdown-item" to="/owning" onClick={closeMobileMenu}>
<i className="bi bi-list-ul me-2"></i>Owning <i className="bi bi-list-ul me-2"></i>Owning
{pendingRequestsCount > 0 && ( {pendingRequestsCount > 0 && (
<span className="badge bg-danger rounded-pill ms-2"> <span className="badge bg-danger rounded-pill ms-2">
@@ -248,17 +300,17 @@ const Navbar: React.FC = () => {
</Link> </Link>
</li> </li>
<li className="d-lg-none"> <li className="d-lg-none">
<Link className="dropdown-item" to="/forum"> <Link className="dropdown-item" to="/forum" onClick={closeMobileMenu}>
<i className="bi bi-chat-dots me-2"></i>Forum <i className="bi bi-chat-dots me-2"></i>Forum
</Link> </Link>
</li> </li>
<li className="d-lg-none"> <li className="d-lg-none">
<Link className="dropdown-item" to="/earnings"> <Link className="dropdown-item" to="/earnings" onClick={closeMobileMenu}>
<i className="bi bi-cash-coin me-2"></i>Earnings <i className="bi bi-cash-coin me-2"></i>Earnings
</Link> </Link>
</li> </li>
<li className="d-lg-none"> <li className="d-lg-none">
<Link className="dropdown-item" to="/messages"> <Link className="dropdown-item" to="/messages" onClick={closeMobileMenu}>
<i className="bi bi-envelope me-2"></i>Messages <i className="bi bi-envelope me-2"></i>Messages
{unreadMessagesCount > 0 && ( {unreadMessagesCount > 0 && (
<span className="badge bg-danger rounded-pill ms-2"> <span className="badge bg-danger rounded-pill ms-2">
@@ -273,21 +325,26 @@ const Navbar: React.FC = () => {
<li className="d-lg-none"> <li className="d-lg-none">
<button <button
className="dropdown-item" className="dropdown-item"
onClick={handleLogout} onClick={() => {
closeMobileMenu();
handleLogout();
}}
> >
<i className="bi bi-box-arrow-right me-2"></i>Logout <i className="bi bi-box-arrow-right me-2"></i>Logout
</button> </button>
</li> </li>
{/* Desktop dropdown */} {/* Desktop dropdown */}
<li className="nav-item dropdown d-none d-lg-block"> <li className="nav-item d-none d-lg-block" ref={dropdownRef} style={{ position: "relative" }}>
<a <button
className="nav-link dropdown-toggle" type="button"
href="#" className="nav-link btn btn-link p-0"
id="navbarDropdown" aria-expanded={isDropdownOpen}
role="button" onClick={(e) => {
data-bs-toggle="dropdown" e.stopPropagation();
aria-expanded="false" setIsDropdownOpen((prev) => !prev);
}}
style={{ cursor: "pointer", border: "none", background: "none" }}
> >
<span <span
style={{ style={{
@@ -322,24 +379,30 @@ const Navbar: React.FC = () => {
)} )}
<Avatar user={user} size="sm" /> <Avatar user={user} size="sm" />
</span> </span>
</a> </button>
{isDropdownOpen && (
<ul <ul
className="dropdown-menu" className="dropdown-menu show"
aria-labelledby="navbarDropdown" style={{
position: "absolute",
right: 0,
top: "100%",
zIndex: 1000,
}}
> >
<li> <li>
<Link className="dropdown-item" to="/profile"> <Link className="dropdown-item" to="/profile" onClick={() => setIsDropdownOpen(false)}>
<i className="bi bi-person me-2"></i>Profile <i className="bi bi-person me-2"></i>Profile
</Link> </Link>
</li> </li>
<li> <li>
<Link className="dropdown-item" to="/renting"> <Link className="dropdown-item" to="/renting" onClick={() => setIsDropdownOpen(false)}>
<i className="bi bi-calendar-check me-2"></i> <i className="bi bi-calendar-check me-2"></i>
Renting Renting
</Link> </Link>
</li> </li>
<li> <li>
<Link className="dropdown-item" to="/owning"> <Link className="dropdown-item" to="/owning" onClick={() => setIsDropdownOpen(false)}>
<i className="bi bi-list-ul me-2"></i>Owning <i className="bi bi-list-ul me-2"></i>Owning
{pendingRequestsCount > 0 && ( {pendingRequestsCount > 0 && (
<span className="badge bg-danger rounded-pill ms-2"> <span className="badge bg-danger rounded-pill ms-2">
@@ -349,19 +412,19 @@ const Navbar: React.FC = () => {
</Link> </Link>
</li> </li>
<li> <li>
<Link className="dropdown-item" to="/forum"> <Link className="dropdown-item" to="/forum" onClick={() => setIsDropdownOpen(false)}>
<i className="bi bi-chat-dots me-2"></i> <i className="bi bi-chat-dots me-2"></i>
Forum Forum
</Link> </Link>
</li> </li>
<li> <li>
<Link className="dropdown-item" to="/earnings"> <Link className="dropdown-item" to="/earnings" onClick={() => setIsDropdownOpen(false)}>
<i className="bi bi-cash-coin me-2"></i> <i className="bi bi-cash-coin me-2"></i>
Earnings Earnings
</Link> </Link>
</li> </li>
<li> <li>
<Link className="dropdown-item" to="/messages"> <Link className="dropdown-item" to="/messages" onClick={() => setIsDropdownOpen(false)}>
<i className="bi bi-envelope me-2"></i>Messages <i className="bi bi-envelope me-2"></i>Messages
{unreadMessagesCount > 0 && ( {unreadMessagesCount > 0 && (
<span className="badge bg-danger rounded-pill ms-2"> <span className="badge bg-danger rounded-pill ms-2">
@@ -376,20 +439,24 @@ const Navbar: React.FC = () => {
<li> <li>
<button <button
className="dropdown-item" className="dropdown-item"
onClick={handleLogout} onClick={() => {
setIsDropdownOpen(false);
handleLogout();
}}
> >
<i className="bi bi-box-arrow-right me-2"></i> <i className="bi bi-box-arrow-right me-2"></i>
Logout Logout
</button> </button>
</li> </li>
</ul> </ul>
)}
</li> </li>
</> </>
) : ( ) : (
<> <>
{/* Mobile menu items */} {/* Mobile menu items */}
<li className="d-lg-none"> <li className="d-lg-none">
<Link className="dropdown-item" to="/forum"> <Link className="dropdown-item" to="/forum" onClick={closeMobileMenu}>
<i className="bi bi-chat-dots me-2"></i>Forum <i className="bi bi-chat-dots me-2"></i>Forum
</Link> </Link>
</li> </li>
@@ -397,7 +464,10 @@ const Navbar: React.FC = () => {
<button <button
className="dropdown-item fw-bold" className="dropdown-item fw-bold"
style={{ color: "#000000" }} style={{ color: "#000000" }}
onClick={() => openAuthModal("login")} onClick={() => {
closeMobileMenu();
openAuthModal("login");
}}
> >
<i className="bi bi-box-arrow-in-right me-2"></i>Login or Sign Up <i className="bi bi-box-arrow-in-right me-2"></i>Login or Sign Up
</button> </button>