-
Notifications
You must be signed in to change notification settings - Fork 205
feat: add Features and About links to navbar #607
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
|
|
||
|
|
@@ -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") { | ||
|
|
@@ -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); | ||
| } | ||
| }; | ||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace clickable Line 70 and Line 143 use clickable 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 |
||
|
|
||
| <NavLink to="/about" className={navLinkStyles}> | ||
| About | ||
| </NavLink> | ||
|
|
||
| <NavLink to="/track" className={navLinkStyles}> | ||
| Tracker | ||
| </NavLink> | ||
|
|
@@ -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" | ||
|
|
@@ -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> | ||
| )} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🤖 Prompt for AI Agents