Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 56 additions & 172 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,173 +1,15 @@
import { NavLink, Link } from "react-router-dom";
import { useEffect, useMemo, useRef, useState, useContext } from "react";
import { NavLink, Link, useNavigate, useLocation } from "react-router-dom";
import { useState, useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";
import { Moon, Sun, Menu, X, ChevronDown, BadgeInfo, LogOut, User } from "lucide-react";

type NavbarUser = {
id?: string;
username?: string;
email?: string;
};

const AUTH_STORAGE_KEY = "github_tracker_auth_user";

const readStoredUser = (): NavbarUser | null => {
if (typeof window === "undefined") {
return null;
}

const storedUser = window.localStorage.getItem(AUTH_STORAGE_KEY);

if (!storedUser) {
return null;
}

try {
const parsedUser = JSON.parse(storedUser) as NavbarUser;
return parsedUser?.username ? parsedUser : null;
} catch {
return null;
}
};

type ProfileDropdownProps = {
user: NavbarUser;
onLogout: () => void;
onCloseMenu?: () => void;
mobile?: boolean;
};

const ProfileDropdown: React.FC<ProfileDropdownProps> = ({ user, onLogout, onCloseMenu, mobile = false }) => {
const [isOpen, setIsOpen] = useState(false);
const profileMenuRef = useRef<HTMLDivElement | null>(null);
const displayName = useMemo(() => user.username ?? "Profile", [user.username]);

useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};

document.addEventListener("mousedown", handleOutsideClick);
return () => document.removeEventListener("mousedown", handleOutsideClick);
}, []);

const closeMenu = () => setIsOpen(false);

if (mobile) {
return (
<div className="mt-2 rounded-3xl border border-gray-200 dark:border-gray-800 bg-gray-50 dark:bg-gray-800/60 p-4">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-600 to-cyan-500 text-white font-semibold">
{displayName.charAt(0).toUpperCase()}
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-white">{displayName}</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{user.email ?? "Signed in"}</p>
</div>
</div>

<div className="mt-4 flex flex-col gap-2">
<Link
to="/profile"
onClick={onCloseMenu}
className="flex items-center gap-2 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-white dark:text-slate-200 dark:hover:bg-white/5"
>
<User className="h-4 w-4" />
View Profile
</Link>
<Link
to={user.username ? `/contributor/${user.username}` : "/contributors"}
onClick={onCloseMenu}
className="flex items-center gap-2 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-white dark:text-slate-200 dark:hover:bg-white/5"
>
<BadgeInfo className="h-4 w-4" />
Account Details
</Link>
<button
onClick={onLogout}
className="flex items-center gap-2 rounded-2xl px-4 py-3 text-sm font-medium text-red-600 transition hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-500/10"
>
<LogOut className="h-4 w-4" />
Logout
</button>
</div>
</div>
);
}

return (
<div className="relative" ref={profileMenuRef}>
<button
onClick={() => setIsOpen((prev) => !prev)}
className="flex items-center gap-3 rounded-2xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 px-3 py-2 text-left transition hover:border-blue-300 hover:bg-blue-50 dark:hover:bg-gray-700"
aria-haspopup="menu"
aria-expanded={isOpen}
>
<span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-600 to-cyan-500 text-white shadow-md">
{displayName.charAt(0).toUpperCase()}
</span>
<span className="hidden xl:block">
<span className="block text-sm font-semibold text-slate-900 dark:text-white">{displayName}</span>
<span className="block text-xs text-slate-500 dark:text-slate-400">Signed in</span>
</span>
<ChevronDown className={`h-4 w-4 text-slate-500 transition-transform ${isOpen ? "rotate-180" : ""}`} />
</button>

{isOpen && (
<div className="absolute right-0 mt-3 w-72 overflow-hidden rounded-3xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 shadow-2xl animate-in fade-in slide-in-from-top-2 duration-200">
<div className="px-5 py-4 border-b border-gray-100 dark:border-gray-800">
<p className="text-xs uppercase tracking-[0.2em] text-slate-400">Account</p>
<div className="mt-2 flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-blue-600 to-cyan-500 text-white font-semibold">
{displayName.charAt(0).toUpperCase()}
</div>
<div>
<p className="font-semibold text-slate-900 dark:text-white">{displayName}</p>
<p className="text-sm text-slate-500 dark:text-slate-400">{user.email ?? "No email available"}</p>
</div>
</div>
</div>

<div className="p-2">
<Link
to="/profile"
onClick={closeMenu}
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-blue-50 hover:text-blue-700 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-cyan-300"
>
<User className="h-4 w-4" />
View Profile
</Link>
<Link
to={user.username ? `/contributor/${user.username}` : "/contributors"}
onClick={closeMenu}
className="flex items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-slate-700 transition hover:bg-blue-50 hover:text-blue-700 dark:text-slate-300 dark:hover:bg-white/5 dark:hover:text-cyan-300"
>
<BadgeInfo className="h-4 w-4" />
Account Details
</Link>
<button
onClick={onLogout}
className="flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-sm font-medium text-red-600 transition hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-500/10"
>
<LogOut className="h-4 w-4" />
Logout
</button>
</div>
</div>
)}
</div>
);
};
import { Moon, Sun, Menu, X } from "lucide-react";

const Navbar: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [user, setUser] = useState<NavbarUser | null>(() => readStoredUser());

const themeContext = useContext(ThemeContext);
const authContext = useContext(AuthContext);
const navigate = useNavigate();
const location = useLocation();

if (!themeContext || !authContext) return null;

Expand All @@ -181,6 +23,9 @@ const Navbar: React.FC = () => {
: "text-slate-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800"
}`;

const featureLinkStyles =
"px-4 py-2 rounded-xl text-sm lg:text-base font-semibold transition-all duration-300 text-slate-700 dark:text-gray-300 hover:text-blue-500 cursor-pointer";

const closeMenu = () => setIsOpen(false);
const handleLogout = () => {
if (typeof window !== "undefined") {
Expand All @@ -201,21 +46,52 @@ const Navbar: React.FC = () => {
}
};

// Smooth scroll to #features on homepage
const handleFeaturesClick = () => {
closeMenu();
if (location.pathname === "/") {
const section = document.getElementById("features");
if (section) {
section.scrollIntoView({ behavior: "smooth" });
}
} else {
navigate("/#features");
setTimeout(() => {
const section = document.getElementById("features");
if (section) section.scrollIntoView({ behavior: "smooth" });
}, 100);
Comment on lines +58 to +62
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid timeout-based scroll sequencing after route change.

Line 39’s fixed setTimeout(100) is race-prone; slower renders can miss the target section and silently fail to scroll. Trigger scrolling from route/hash change instead of a time delay.

Suggested fix
-import { useState, useContext } from "react";
+import { useState, useContext, useEffect } from "react";
...
   const handleFeaturesClick = () => {
     closeMenu();
     if (location.pathname === "/") {
       const section = document.getElementById("features");
       if (section) {
         section.scrollIntoView({ behavior: "smooth" });
       }
     } else {
-      navigate("/#features");
-      setTimeout(() => {
-        const section = document.getElementById("features");
-        if (section) section.scrollIntoView({ behavior: "smooth" });
-      }, 100);
+      navigate("/#features");
     }
   };
+
+  useEffect(() => {
+    if (location.pathname === "/" && location.hash === "`#features`") {
+      const section = document.getElementById("features");
+      if (section) {
+        section.scrollIntoView({ behavior: "smooth" });
+      }
+    }
+  }, [location.pathname, location.hash]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Navbar.tsx` around lines 38 - 42, The timeout-based scroll
after calling navigate("/#features") is race-prone; instead remove the
setTimeout and move the scroll logic to run when the route/hash actually changes
(for example, in the component that renders the "features" section or a
top-level effect that watches location.hash). Detect the hash (location.hash or
listen to "hashchange") and when it equals "`#features`" call
document.getElementById("features") and section.scrollIntoView({ behavior:
"smooth" }); (or run this on mount via useEffect in the Features-containing
component) rather than relying on setTimeout in the Navbar navigate handler.

}
};

return (
<nav className="sticky top-0 z-50 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 transition-colors duration-300 backdrop-blur">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<Link
to="/"
className="flex items-center gap-3 text-xl font-bold text-slate-900 dark:text-white"
>
<img src="/crl-icon.png" alt="CRL Icon" className="h-8 w-8 object-contain" />
<img
src="/crl-icon.png"
alt="CRL Icon"
className="h-8 w-8 object-contain"
/>
<span>GitHub Tracker</span>
</Link>

<div className="hidden md:flex items-center gap-3">
<NavLink to="/" className={navLinkStyles}>
<NavLink to="/" end className={navLinkStyles}>
Home
</NavLink>

{/* Features: smooth scroll to #features section on homepage */}
<span className={featureLinkStyles} onClick={handleFeaturesClick}>
Features
</span>
Comment on lines +87 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace clickable <span> with semantic interactive controls.

Line 70 and Line 143 use clickable <span> elements, which are not keyboard-operable by default and miss expected navigation semantics. This is an accessibility blocker for keyboard users.

Suggested fix
-          <span className={featureLinkStyles} onClick={handleFeaturesClick}>
+          <button
+            type="button"
+            className={`${featureLinkStyles} focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500`}
+            onClick={handleFeaturesClick}
+          >
             Features
-          </span>
+          </button>
-            <span className={featureLinkStyles} onClick={handleFeaturesClick}>
+            <button
+              type="button"
+              className={`${featureLinkStyles} text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500`}
+              onClick={handleFeaturesClick}
+            >
               Features
-            </span>
+            </button>

Also applies to: 143-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Navbar.tsx` around lines 70 - 72, Replace the non-semantic,
non-keyboard-operable <span> elements used for interactive navigation with
proper interactive elements (e.g., <button> or <a>) so they are
keyboard-accessible and provide correct semantics; specifically update the
element using className featureLinkStyles and onClick={handleFeaturesClick} (and
the second similar span at lines ~143-145) to use a button or anchor, preserve
existing event handlers (handleFeaturesClick), add appropriate ARIA attributes
and keyboard handlers if needed, and ensure styling still applies via
featureLinkStyles so visual appearance is unchanged.


<NavLink to="/about" className={navLinkStyles}>
About
</NavLink>

<NavLink to="/track" className={navLinkStyles}>
Tracker
</NavLink>
Expand Down Expand Up @@ -245,6 +121,7 @@ const Navbar: React.FC = () => {
</div>

<div className="md:hidden flex items-center gap-2">
{/* Theme Toggle */}
<button
onClick={toggleTheme}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
Expand Down Expand Up @@ -273,23 +150,30 @@ const Navbar: React.FC = () => {
{isOpen && (
<div className="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900">
<div className="px-6 py-5 flex flex-col gap-3">
<NavLink to="/" className={navLinkStyles} onClick={closeMenu}>
<NavLink to="/" end className={navLinkStyles} onClick={closeMenu}>
Home
</NavLink>

{/* Features: smooth scroll to #features section on homepage */}
<span className={featureLinkStyles} onClick={handleFeaturesClick}>
Features
</span>

<NavLink to="/about" className={navLinkStyles} onClick={closeMenu}>
About
</NavLink>

<NavLink to="/track" className={navLinkStyles} onClick={closeMenu}>
Tracker
</NavLink>

<NavLink to="/contributors" className={navLinkStyles} onClick={closeMenu}>
Contributors
</NavLink>

{user ? (
<ProfileDropdown user={user} onLogout={handleLogout} onCloseMenu={closeMenu} mobile />
) : (
<NavLink to="/login" className={navLinkStyles} onClick={closeMenu}>
Login
</NavLink>
)}
<NavLink to="/login" className={navLinkStyles} onClick={closeMenu}>
Login
</NavLink>
</div>
</div>
)}
Expand Down
Loading