Skip to content
Open
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions src/Routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.t
import Home from "../pages/Home/Home.tsx";
import Activity from "../pages/Activity.tsx";
import PrivacyPolicy from "../pages/Privacy/PrivacyPolicy.tsx"; // ✅ Updated import path to match your new folder structure
import Profile from "../pages/Profile/Profile.tsx";
import ProtectedRoute from "../components/ProtectedRoute";

const Router = () => {
return (
Expand All @@ -21,6 +23,7 @@ const Router = () => {
<Route path="/contact" element={<Contact />} />
<Route path="/contributors" element={<Contributors />} />
<Route path="/contributor/:username" element={<ContributorProfile />} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/activity" element={<Activity />} />

{/* Privacy Policy page route */}
Expand Down
234 changes: 186 additions & 48 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,169 @@
import { NavLink, Link } from "react-router-dom";
import { useState, useContext } from "react";
import { useEffect, useMemo, useRef, useState, useContext } from "react";
import { ThemeContext } from "../context/ThemeContext";
import { Moon, Sun, Menu, X, Github } from "lucide-react";
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>
);
};

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

const themeContext = useContext(ThemeContext);

Expand All @@ -20,44 +179,44 @@ const Navbar: React.FC = () => {
}`;

const closeMenu = () => setIsOpen(false);
const handleLogout = () => {
if (typeof window !== "undefined") {
window.localStorage.removeItem(AUTH_STORAGE_KEY);
}
setUser(null);
closeMenu();
};

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">

{/* Logo */}
<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>

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

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

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

<NavLink to="/login" className={navLinkStyles}>
Login
</NavLink>
{user ? (
<ProfileDropdown user={user} onLogout={handleLogout} />
) : (
<NavLink to="/login" className={navLinkStyles}>
Login
</NavLink>
)}

{/* Theme Toggle */}
<button
onClick={toggleTheme}
className="ml-2 p-2 rounded-xl border border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
Expand All @@ -71,10 +230,7 @@ const Navbar: React.FC = () => {
</button>
</div>

{/* Mobile Controls */}
<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 All @@ -86,8 +242,6 @@ const Navbar: React.FC = () => {
<Moon className="h-5 w-5 text-white" />
)}
</button>

{/* Menu Toggle */}
<button
onClick={() => setIsOpen(!isOpen)}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
Expand All @@ -102,42 +256,26 @@ const Navbar: React.FC = () => {
</div>
</div>

{/* Mobile Menu */}
{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="/" className={navLinkStyles} onClick={closeMenu}>
Home
</NavLink>

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

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

<NavLink
to="/login"
className={navLinkStyles}
onClick={closeMenu}
>
Login
</NavLink>
{user ? (
<ProfileDropdown user={user} onLogout={handleLogout} onCloseMenu={closeMenu} mobile />
) : (
<NavLink to="/login" className={navLinkStyles} onClick={closeMenu}>
Login
</NavLink>
)}
</div>
</div>
)}
Expand Down
37 changes: 37 additions & 0 deletions src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { ReactNode } from "react";
import { Navigate } from "react-router-dom";

type ProtectedRouteProps = {
children: ReactNode;
};

const AUTH_STORAGE_KEY = "github_tracker_auth_user";

const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
const isAuthenticated = (() => {
if (typeof window === "undefined") {
return false;
}

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

if (!storedUser) {
return false;
}

try {
const parsedUser = JSON.parse(storedUser) as { username?: string; email?: string };
return Boolean(parsedUser?.username && parsedUser?.email);
} catch {
return false;
}
})();

if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}

return <>{children}</>;
};

export default ProtectedRoute;
Loading