diff --git a/package.json b/package.json index 43ad31cc..5d166404 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "GitHub Tracker", + "name": "github-tracker", "private": true, "version": "0.0.0", "type": "module", diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index fd5eac86..596a3244 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,7 +1,7 @@ import { NavLink, Link } from "react-router-dom"; import { useState, useContext } from "react"; import { ThemeContext } from "../context/ThemeContext"; -import { Moon, Sun, Menu, X, Github } from "lucide-react"; +import { Moon, Sun, Menu, X } from "lucide-react"; const Navbar: React.FC = () => { const [isOpen, setIsOpen] = useState(false); diff --git a/src/components/__test__/Navbar.test.tsx b/src/components/__test__/Navbar.test.tsx index 780b9bb3..60bc0712 100644 --- a/src/components/__test__/Navbar.test.tsx +++ b/src/components/__test__/Navbar.test.tsx @@ -1,6 +1,6 @@ // src/components/__tests__/Navbar.test.tsx import { render, screen, fireEvent } from '@testing-library/react' -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { MemoryRouter } from 'react-router-dom' import { ThemeContext } from "../../context/ThemeContext"; import Navbar from '../Navbar.tsx' @@ -51,31 +51,30 @@ describe('Navbar', () => { // --- Mobile menu --- it('mobile menu is hidden by default', () => { renderNavbar() - expect(screen.queryByText('About')).not.toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /^tracker$/i })).toHaveLength(1) }) it('opens mobile menu when hamburger is clicked', () => { renderNavbar() - const hamburger = screen.getAllByRole('button')[1] // second button = hamburger + const hamburger = screen.getAllByRole('button')[2] // third button = hamburger fireEvent.click(hamburger) - expect(screen.getByText('About')).toBeInTheDocument() - expect(screen.getByText('Contact')).toBeInTheDocument() + expect(screen.getAllByRole('link', { name: /^tracker$/i })).toHaveLength(2) }) it('closes mobile menu when a nav link is clicked', () => { renderNavbar() - const hamburger = screen.getAllByRole('button')[1] + const hamburger = screen.getAllByRole('button')[2] fireEvent.click(hamburger) // open + expect(screen.getAllByRole('link', { name: /^tracker$/i })).toHaveLength(2) const homeLinks = screen.getAllByRole('link', { name: /home/i }) fireEvent.click(homeLinks[homeLinks.length - 1]) // click the mobile one - expect(screen.queryByText('About')).not.toBeInTheDocument() // closed + expect(screen.getAllByRole('link', { name: /^tracker$/i })).toHaveLength(1) // closed }) - it('calls toggleTheme from the mobile menu button', () => { + it('calls toggleTheme from the mobile theme button', () => { const { toggleTheme } = renderNavbar('dark') - const hamburger = screen.getAllByRole('button')[1] - fireEvent.click(hamburger) - fireEvent.click(screen.getByText(/light/i)) + const mobileThemeBtn = screen.getAllByRole('button')[1] // second button = mobile theme toggle + fireEvent.click(mobileThemeBtn) expect(toggleTheme).toHaveBeenCalledTimes(1) }) diff --git a/src/hooks/useGitHubAuth.ts b/src/hooks/useGitHubAuth.ts index a0c24b2a..3044fa65 100644 --- a/src/hooks/useGitHubAuth.ts +++ b/src/hooks/useGitHubAuth.ts @@ -1,25 +1,54 @@ -import { useState, useMemo } from 'react'; +import { useState, useEffect } from 'react'; import { Octokit } from '@octokit/core'; export const useGitHubAuth = () => { - const [username, setUsername] = useState(''); - const [token, setToken] = useState(''); + const [username, setUsername] = useState(() => sessionStorage.getItem('tracker_username') || ''); + const [token, setToken] = useState(() => sessionStorage.getItem('tracker_token') || ''); + const [error, setError] = useState(''); - const octokit = useMemo(() => { - if (!username) return null; - if(token){ - return new Octokit({ auth: token }); + useEffect(() => { + if (username) { + sessionStorage.setItem('tracker_username', username); + } else { + sessionStorage.removeItem('tracker_username'); + } + if (token) { + sessionStorage.setItem('tracker_token', token); + } else { + sessionStorage.removeItem('tracker_token'); } - return new Octokit(); }, [username, token]); - const getOctokit = () => octokit; + const getOctokit = () => { + try { + setError(''); + if (!username) return null; + if (token) { + return new Octokit({ auth: token }); + } + return new Octokit(); + } catch (err: any) { + setError(err instanceof Error ? err.message : String(err)); + return null; + } + }; + + const logout = () => { + setUsername(''); + setToken(''); + setError(''); + sessionStorage.removeItem('tracker_username'); + sessionStorage.removeItem('tracker_token'); + }; return { username, setUsername, token, setToken, + error, + setError, getOctokit, + logout, }; }; diff --git a/src/hooks/useGitHubData.ts b/src/hooks/useGitHubData.ts index f4c78cf6..2953e4e7 100644 --- a/src/hooks/useGitHubData.ts +++ b/src/hooks/useGitHubData.ts @@ -239,6 +239,17 @@ export const useGitHubData = ( [getOctokit, rateLimited] ); + const clearData = useCallback(() => { + lastRequestId.current++; + setLoading(false); + setIssues([]); + setPrs([]); + setTotalIssues(0); + setTotalPrs(0); + setError(''); + setRateLimited(false); + }, []); + return { issues, prs, @@ -248,5 +259,6 @@ export const useGitHubData = ( error, rateLimited, fetchData, + clearData, }; }; diff --git a/src/pages/Tracker/Tracker.tsx b/src/pages/Tracker/Tracker.tsx index 576f39bf..5ee74dce 100644 --- a/src/pages/Tracker/Tracker.tsx +++ b/src/pages/Tracker/Tracker.tsx @@ -57,6 +57,7 @@ const Home: React.FC = () => { setToken, error: authError, getOctokit, + logout, } = useGitHubAuth(); const { @@ -67,6 +68,7 @@ const Home: React.FC = () => { loading, error: dataError, fetchData, + clearData, } = useGitHubData(getOctokit); const [tab, setTab] = useState(0); @@ -79,6 +81,32 @@ const Home: React.FC = () => { const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); + const handleResetFilters = () => { + setTab(0); + setPage(0); + setIssueFilter("all"); + setPrFilter("all"); + setSearchTitle(""); + setSelectedRepo(""); + setStartDate(""); + setEndDate(""); + + localStorage.removeItem('tracker_tab'); + localStorage.removeItem('tracker_page'); + localStorage.removeItem('tracker_issueFilter'); + localStorage.removeItem('tracker_prFilter'); + localStorage.removeItem('tracker_searchTitle'); + localStorage.removeItem('tracker_selectedRepo'); + localStorage.removeItem('tracker_startDate'); + localStorage.removeItem('tracker_endDate'); + }; + + const handleLogout = () => { + logout(); + clearData(); + handleResetFilters(); + }; + // Fetch data when username, tab, or page changes useEffect(() => { if (username) { @@ -238,6 +266,21 @@ const Home: React.FC = () => { > Fetch Data + {(username || token) && ( + + )} @@ -272,6 +315,18 @@ const Home: React.FC = () => { InputLabelProps={{ shrink: true }} sx={{ minWidth: 150 }} /> + {(tab !== 0 || page !== 0 || issueFilter !== "all" || prFilter !== "all" || searchTitle !== "" || selectedRepo !== "" || startDate !== "" || endDate !== "") && ( + + )} {/* Tabs + State Filter */}