Files
rentall-app/frontend/src/components/Navbar.tsx
2025-12-22 22:35:57 -05:00

327 lines
12 KiB
TypeScript

import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import { useSocket } from "../contexts/SocketContext";
import { rentalAPI, messageAPI } from "../services/api";
const Navbar: React.FC = () => {
const { user, logout, openAuthModal } = useAuth();
const { onNewMessage, onMessageRead } = useSocket();
const navigate = useNavigate();
const [searchFilters, setSearchFilters] = useState({
search: "",
location: "",
});
const [pendingRequestsCount, setPendingRequestsCount] = useState(0);
const [unreadMessagesCount, setUnreadMessagesCount] = useState(0);
// Fetch pending rental requests count when user logs in
useEffect(() => {
const fetchPendingCount = async () => {
if (user) {
try {
const response = await rentalAPI.getPendingRequestsCount();
setPendingRequestsCount(response.data.count);
} catch (error) {
console.error("Failed to fetch pending requests count:", error);
}
} else {
setPendingRequestsCount(0);
}
};
fetchPendingCount();
// Listen for rental status changes to refresh count
const handleRentalStatusChange = () => {
fetchPendingCount();
};
window.addEventListener("rentalStatusChanged", handleRentalStatusChange);
return () => {
window.removeEventListener("rentalStatusChanged", handleRentalStatusChange);
};
}, [user]);
// Fetch unread messages count when user logs in
useEffect(() => {
const fetchUnreadCount = async () => {
if (user) {
try {
const response = await messageAPI.getUnreadCount();
setUnreadMessagesCount(response.data.count);
} catch (error) {
console.error("Failed to fetch unread message count:", error);
}
} else {
setUnreadMessagesCount(0);
}
};
fetchUnreadCount();
}, [user]);
// Listen for real-time message updates via socket
useEffect(() => {
if (!user) return;
// Listen for new messages
const cleanupNewMessage = onNewMessage((message: any) => {
if (message.receiverId === user.id) {
setUnreadMessagesCount((prev) => prev + 1);
}
});
// Listen for messages being read
const cleanupMessageRead = onMessageRead(() => {
setUnreadMessagesCount((prev) => Math.max(0, prev - 1));
});
return () => {
cleanupNewMessage();
cleanupMessageRead();
};
}, [user, onNewMessage, onMessageRead]);
const handleLogout = () => {
logout();
navigate("/");
};
const handleSearch = (e?: React.FormEvent | React.MouseEvent) => {
e?.preventDefault();
const params = new URLSearchParams();
if (searchFilters.search.trim()) {
params.append("search", searchFilters.search.trim());
}
if (searchFilters.location.trim()) {
// Check if location looks like a zip code (5 digits) or city name
const location = searchFilters.location.trim();
if (/^\d{5}(-\d{4})?$/.test(location)) {
params.append("zipCode", location);
} else {
params.append("city", location);
}
}
const queryString = params.toString();
navigate(`/items${queryString ? `?${queryString}` : ""}`);
// Clear search after navigating
setSearchFilters({ search: "", location: "" });
};
const handleSearchInputChange = (
field: "search" | "location",
value: string
) => {
setSearchFilters((prev) => ({ ...prev, [field]: value }));
};
return (
<>
<nav className="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
<div className="container-fluid" style={{ maxWidth: "1800px" }}>
<Link className="navbar-brand fw-bold" to="/">
<i className="bi bi-box-seam me-2"></i>
Village Share
</Link>
<button
className="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarNav">
<div className="d-flex align-items-center w-100">
<div className="position-absolute start-50 translate-middle-x">
<div>
<div className="input-group" style={{ width: "520px" }}>
<input
type="text"
className="form-control"
placeholder="Search items..."
value={searchFilters.search}
onChange={(e) =>
handleSearchInputChange("search", e.target.value)
}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSearch(e);
}
}}
/>
<span
className="input-group-text text-muted"
style={{
borderLeft: "0",
borderRight: "1px solid #dee2e6",
backgroundColor: "#f8f9fa",
}}
>
in
</span>
<input
type="text"
className="form-control"
placeholder="City or ZIP"
value={searchFilters.location}
onChange={(e) =>
handleSearchInputChange("location", e.target.value)
}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSearch(e);
}
}}
/>
<button
className="btn btn-outline-secondary"
type="button"
onClick={handleSearch}
>
<i className="bi bi-search"></i>
</button>
</div>
</div>
</div>
<div className="ms-auto d-flex align-items-center">
<Link
className="btn btn-outline-primary btn-sm me-3 text-nowrap"
to="/create-item"
>
Start Earning
</Link>
<ul className="navbar-nav flex-row">
{user ? (
<>
<li className="nav-item dropdown">
<a
className="nav-link dropdown-toggle"
href="#"
id="navbarDropdown"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<span style={{ display: "flex", alignItems: "center", position: "relative" }}>
{(pendingRequestsCount > 0 || unreadMessagesCount > 0) && (
<span
style={{
position: "absolute",
left: "-0.9em",
top: "50%",
transform: "translateY(-50%)",
backgroundColor: "#dc3545",
color: "white",
borderRadius: "50%",
width: "1.5em",
height: "1.5em",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.85em",
fontWeight: "bold",
border: "2px solid white",
opacity: 1,
zIndex: 1,
}}
>
{pendingRequestsCount + unreadMessagesCount}
</span>
)}
<i className="bi bi-person-circle me-1"></i>
{user.firstName}
</span>
</a>
<ul
className="dropdown-menu"
aria-labelledby="navbarDropdown"
>
<li>
<Link className="dropdown-item" to="/profile">
<i className="bi bi-person me-2"></i>Profile
</Link>
</li>
<li>
<Link className="dropdown-item" to="/renting">
<i className="bi bi-calendar-check me-2"></i>
Renting
</Link>
</li>
<li>
<Link className="dropdown-item" to="/owning">
<i className="bi bi-list-ul me-2"></i>Owning
{pendingRequestsCount > 0 && (
<span className="badge bg-danger rounded-pill ms-2">
{pendingRequestsCount}
</span>
)}
</Link>
</li>
<li>
<Link className="dropdown-item" to="/forum">
<i className="bi bi-chat-dots me-2"></i>
Forum
</Link>
</li>
<li>
<Link className="dropdown-item" to="/earnings">
<i className="bi bi-cash-coin me-2"></i>
Earnings
</Link>
</li>
<li>
<Link className="dropdown-item" to="/messages">
<i className="bi bi-envelope me-2"></i>Messages
{unreadMessagesCount > 0 && (
<span className="badge bg-danger rounded-pill ms-2">
{unreadMessagesCount}
</span>
)}
</Link>
</li>
<li>
<hr className="dropdown-divider" />
</li>
<li>
<button
className="dropdown-item"
onClick={handleLogout}
>
<i className="bi bi-box-arrow-right me-2"></i>
Logout
</button>
</li>
</ul>
</li>
</>
) : (
<li className="nav-item">
<button
className="btn btn-primary btn-sm text-nowrap"
onClick={() => openAuthModal("login")}
>
Login or Sign Up
</button>
</li>
)}
</ul>
</div>
</div>
</div>
</div>
</nav>
</>
);
};
export default Navbar;