From 7ccb6eef0e62021a8ae4b38fd2ce7c907c30ab3a Mon Sep 17 00:00:00 2001 From: Nishanth <138886231+nishanthkj@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:35:53 +0000 Subject: [PATCH] feat(profile): add Profile page, AuthContext and navbar dropdown with logout Add app-wide AuthContext and Profile page, wire into navbar and routing Changes: - src/context/AuthContext.tsx: new Auth provider that stores GitHub username and personal access token, persists to localStorage, exposes getOctokit() and logout(). - src/pages/Profile/Profile.tsx: new page to view/edit GitHub username and token. Saves to AuthContext and navigates back on save. - src/Routes/Router.tsx: added /profile route. - src/components/Navbar.tsx: updated header to show username when authenticated, with a dropdown containing View/Edit Profile and Logout. Mobile menu updated similarly. - src/main.tsx: app wrapped with AuthProvider so auth is available globally. - src/pages/Tracker/Tracker.tsx: switched to using AuthContext for username/token and getOctokit instead of local hook, so auth is centralized. Rationale: Centralized auth state enables consistent UX (navbar profile dropdown, logout, and profile editing) and avoids duplicating token/username state across pages. Storing credentials in localStorage provides a simple persistence layer; consider a more secure approach for production. Testing/Validation: - Ran a production build (vite build) locally; build succeeded. Notes and next steps: - Consider adding avatar fetch for the navbar, logout confirmation, and secure storage for PATs. --- src/Routes/Router.tsx | 2 + src/components/Navbar.tsx | 84 ++++++++++++++++++++++++++++++++--- src/context/AuthContext.tsx | 74 ++++++++++++++++++++++++++++++ src/main.tsx | 9 ++-- src/pages/Profile/Profile.tsx | 63 ++++++++++++++++++++++++++ src/pages/Tracker/Tracker.tsx | 19 ++++---- 6 files changed, 233 insertions(+), 18 deletions(-) create mode 100644 src/context/AuthContext.tsx create mode 100644 src/pages/Profile/Profile.tsx diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 40a7861..53f6780 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -6,6 +6,7 @@ import Contributors from "../pages/Contributors/Contributors"; import Signup from "../pages/Signup/Signup.tsx"; import Login from "../pages/Login/Login.tsx"; import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; +import Profile from "../pages/Profile/Profile"; import Home from "../pages/Home/Home.tsx"; const Router = () => { @@ -18,6 +19,7 @@ const Router = () => { } /> } /> } /> + } /> } /> ); diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index c6cc86d..b04522b 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,8 @@ import { Link } from "react-router-dom"; import { useState, useContext } from "react"; import { ThemeContext } from "../context/ThemeContext"; +import { AuthContext } from "../context/AuthContext"; +import { useNavigate } from "react-router-dom"; import { Moon, Sun } from 'lucide-react'; @@ -8,6 +10,8 @@ const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false); const themeContext = useContext(ThemeContext); + const auth = useContext(AuthContext); + const navigate = useNavigate(); if (!themeContext) return null; @@ -46,12 +50,57 @@ const Navbar: React.FC = () => { > Contributors - - Login - + {auth && auth.username ? ( + + setIsOpen((v) => !v)} + className="text-lg font-medium px-2 py-1 border border-transparent rounded flex items-center space-x-2" + > + {auth.username} + + + + + + {/* Dropdown */} + {isOpen && ( + + { + setIsOpen(false); + navigate('/profile'); + }} + className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600" + > + View / Edit Profile + + { + setIsOpen(false); + auth.logout(); + navigate('/login'); + }} + className="block w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600" + > + Logout + + + )} + + ) : ( + + Login + + )} { > Login + {auth && auth.username && ( + <> + { + setIsOpen(false); + navigate('/profile'); + }} + className="block text-left w-full text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded" + > + View / Edit Profile + + { + setIsOpen(false); + auth.logout(); + navigate('/login'); + }} + className="block text-left w-full text-lg font-medium hover:text-gray-300 transition-all px-2 py-1 border border-transparent hover:border-gray-400 rounded" + > + Logout + + > + )} { toggleTheme(); diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..4f513a3 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,74 @@ +import React, { createContext, useState, useEffect } from "react"; +import { Octokit } from "octokit"; + +type AuthContextType = { + username: string; + token: string; + setUsername: (u: string) => void; + setToken: (t: string) => void; + logout: () => void; + getOctokit: () => any | null; +}; + +export const AuthContext = createContext(null); + +const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [username, setUsernameState] = useState(""); + const [token, setTokenState] = useState(""); + + useEffect(() => { + // hydrate from localStorage if present + try { + const s = localStorage.getItem("gh_username"); + const t = localStorage.getItem("gh_token"); + if (s) setUsernameState(s); + if (t) setTokenState(t); + } catch (e) { + // ignore + } + }, []); + + const setUsername = (u: string) => { + setUsernameState(u); + try { + if (u) localStorage.setItem("gh_username", u); + else localStorage.removeItem("gh_username"); + } catch (e) {} + }; + + const setToken = (t: string) => { + setTokenState(t); + try { + if (t) localStorage.setItem("gh_token", t); + else localStorage.removeItem("gh_token"); + } catch (e) {} + }; + + const logout = () => { + setUsernameState(""); + setTokenState(""); + try { + localStorage.removeItem("gh_username"); + localStorage.removeItem("gh_token"); + } catch (e) {} + }; + + const getOctokit = () => { + if (!username || !token) return null; + try { + return new Octokit({ auth: token }); + } catch (e) { + return null; + } + }; + + return ( + + {children} + + ); +}; + +export default AuthProvider; diff --git a/src/main.tsx b/src/main.tsx index 4c5b79d..8db1f8e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,13 +4,16 @@ import App from "./App.tsx"; import "./index.css"; import { BrowserRouter } from "react-router-dom"; import ThemeWrapper from "./context/ThemeContext.tsx"; +import AuthProvider from "./context/AuthContext"; createRoot(document.getElementById("root")!).render( - - - + + + + + ); \ No newline at end of file diff --git a/src/pages/Profile/Profile.tsx b/src/pages/Profile/Profile.tsx new file mode 100644 index 0000000..5f7e425 --- /dev/null +++ b/src/pages/Profile/Profile.tsx @@ -0,0 +1,63 @@ +import React, { useContext, useState } from "react"; +import { AuthContext } from "../../context/AuthContext"; +import { useNavigate } from "react-router-dom"; + +const Profile: React.FC = () => { + const auth = useContext(AuthContext); + const navigate = useNavigate(); + + if (!auth) return null; + + const { username, token, setUsername, setToken } = auth; + + const [localUsername, setLocalUsername] = useState(username || ""); + const [localToken, setLocalToken] = useState(token || ""); + + const handleSave = (e: React.FormEvent) => { + e.preventDefault(); + setUsername(localUsername); + setToken(localToken); + navigate("/"); + }; + + return ( + + Profile + + + GitHub Username + setLocalUsername(e.target.value)} + required + /> + + + + Personal Access Token + setLocalToken(e.target.value)} + type="password" + placeholder="••••••••" + /> + + + + Save + navigate(-1)} + className="px-4 py-2 border rounded" + > + Cancel + + + + + ); +}; + +export default Profile; diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 2bd4d30..16e1f1c 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -30,7 +30,8 @@ import { InputLabel, } from "@mui/material"; import { useTheme } from "@mui/material/styles"; -import { useGitHubAuth } from "../../hooks/useGitHubAuth"; +import { useContext } from "react"; +import { AuthContext } from "../../context/AuthContext"; import { useGitHubData } from "../../hooks/useGitHubData"; const ROWS_PER_PAGE = 10; @@ -49,14 +50,14 @@ const Home: React.FC = () => { const theme = useTheme(); - const { - username, - setUsername, - token, - setToken, - error: authError, - getOctokit, - } = useGitHubAuth(); + const auth = useContext(AuthContext); + + const username = auth?.username || ""; + const setUsername = auth?.setUsername || (() => {}); + const token = auth?.token || ""; + const setToken = auth?.setToken || (() => {}); + const authError = ""; + const getOctokit = auth?.getOctokit || (() => null); const { issues,