From ea8174dbf2b654bbce19cd674758c223e9ad66ba Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 15:19:58 +0530 Subject: [PATCH 01/11] feat: add mobile navigation menu --- src/components/Navbar.jsx | 114 ++++++++++++++++++++++++++++++++++++-- src/styles/global.css | 32 +++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index bdafb79e..3dda7995 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,6 +1,6 @@ -import React from 'react' +import React, { useState } from 'react' import { NavLink, useNavigate } from 'react-router-dom' -import { FiHeart, FiSettings, FiZap } from 'react-icons/fi' +import { FiHeart, FiSettings, FiZap, FiMenu, FiX } from 'react-icons/fi' import { useApp } from '../context/AppContext' import ThemeToggle from './ThemeToggle' import Logo from "../assets/og-logo.svg?react"; @@ -19,9 +19,12 @@ export default function Navbar() { const { orgs, rateLimit } = useApp() const { theme } = useTheme(); const navigate = useNavigate() + const [menuOpen, setMenuOpen] = useState(false) const hasData = orgs.length > 0 const lowLimit = rateLimit && rateLimit.remaining < 15 + const closeMenu = () => setMenuOpen(false) + return ( ) } + diff --git a/src/styles/global.css b/src/styles/global.css index d6f6d516..f1e19c5c 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -80,4 +80,36 @@ input[type="date"]::-webkit-calendar-picker-indicator { .navbar-link:hover { color: var(--accent) !important; +} + +/* Responsive navbar styles */ +.navbar-hamburger { + display: none; +} + +@media (max-width: 768px) { + .navbar-links { + display: none !important; + } + .navbar-controls { + display: none !important; + } + .navbar-hamburger { + display: flex !important; + } +} + +.navbar-mobile-menu { + position: absolute; + top: 56px; + left: 0; + right: 0; + background: var(--bg); + border-bottom: 1px solid var(--border); + padding: 16px 24px; + display: flex; + flex-direction: column; + gap: 12px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + z-index: 99; } \ No newline at end of file From 3251d66742840d2d4870a7359b01face674995cb Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 15:30:10 +0530 Subject: [PATCH 02/11] feat: constrain responsive application shell --- src/App.jsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 56d854d2..15576553 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -19,10 +19,19 @@ import RequireAnalysis from './components/RequireAnalysis' function Layout({ children }) { return ( -
+
-
{children}
+
+ {children} +
) From 4a5e6c6a67de3ca8be370ea8940a9f18e3cd24e3 Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 16:16:42 +0530 Subject: [PATCH 03/11] feat: add global mobile CSS resets --- src/styles/global.css | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/styles/global.css b/src/styles/global.css index f1e19c5c..037f6fa1 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -35,13 +35,41 @@ --radius: 8px; } +/* Base & Mobile Reset */ +*, *::before, *::after { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; +} + +html { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; +} + html, body, #root { + width: 100%; + max-width: 100%; min-height: 100%; background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +img, +svg, +video, +canvas { + max-width: 100%; + height: auto; } button { cursor: pointer; font-family: inherit; } From 69ad831edac66d985d7f25c407f3a225983935ff Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 16:55:53 +0530 Subject: [PATCH 04/11] fix: improve mobile navbar responsiveness --- src/components/Navbar.jsx | 38 +++++++- src/components/Navbar.test.jsx | 142 +++++++++++++++++++++++++++++ src/components/RateLimitBanner.jsx | 2 + 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/components/Navbar.test.jsx diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 3dda7995..702d7e14 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react' -import { NavLink, useNavigate } from 'react-router-dom' +import React, { useState, useEffect } from 'react' +import { NavLink, useNavigate, useLocation } from 'react-router-dom' import { FiHeart, FiSettings, FiZap, FiMenu, FiX } from 'react-icons/fi' import { useApp } from '../context/AppContext' import ThemeToggle from './ThemeToggle' @@ -19,12 +19,28 @@ export default function Navbar() { const { orgs, rateLimit } = useApp() const { theme } = useTheme(); const navigate = useNavigate() + const location = useLocation() const [menuOpen, setMenuOpen] = useState(false) const hasData = orgs.length > 0 const lowLimit = rateLimit && rateLimit.remaining < 15 const closeMenu = () => setMenuOpen(false) + // Close mobile menu on route change + useEffect(() => { + setMenuOpen(false) + }, [location.pathname]) + + // Close mobile menu on Escape key press + useEffect(() => { + if (!menuOpen) return + const handleKeyDown = e => { + if (e.key === 'Escape') setMenuOpen(false) + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [menuOpen]) + return (
)} + + {/* Backdrop overlay */} + {menuOpen && ( +
+ )} ) } + diff --git a/src/components/Navbar.test.jsx b/src/components/Navbar.test.jsx new file mode 100644 index 00000000..1e193739 --- /dev/null +++ b/src/components/Navbar.test.jsx @@ -0,0 +1,142 @@ +import React from 'react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter } from 'react-router-dom' +import Navbar from './Navbar' + +const mockAppState = { + orgs: [{ login: 'AOSSIE-Org' }], + rateLimit: { remaining: 50, limit: 60 }, +} + +const mockThemeState = { + theme: 'dark', + toggleTheme: vi.fn(), +} + +vi.mock('../context/AppContext', () => ({ + useApp: () => mockAppState, +})) + +vi.mock('../context/ThemeContext', () => ({ + useTheme: () => mockThemeState, +})) + +function renderNavbar(initialRoute = '/') { + return render( + + + + ) +} + +describe('Navbar Mobile Navigation', () => { + beforeEach(() => { + mockAppState.orgs = [{ login: 'AOSSIE-Org' }] + mockAppState.rateLimit = { remaining: 50, limit: 60 } + }) + + it('renders hamburger button with initial aria-expanded="false"', () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + expect(hamburger).toBeInTheDocument() + expect(hamburger).toHaveAttribute('aria-expanded', 'false') + expect(hamburger).toHaveAttribute('aria-controls', 'mobile-nav-menu') + expect(screen.queryByTestId('navbar-backdrop')).not.toBeInTheDocument() + expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() + }) + + it('opens mobile menu and sets aria-expanded="true" when hamburger is clicked', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + + expect(hamburger).toHaveAttribute('aria-expanded', 'true') + expect(document.getElementById('mobile-nav-menu')).toBeInTheDocument() + expect(screen.getByTestId('navbar-backdrop')).toBeInTheDocument() + }) + + it('renders navigation links and action controls in the mobile menu', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + + const mobileMenu = document.getElementById('mobile-nav-menu') + expect(mobileMenu).toBeInTheDocument() + + // Check links within mobile menu + expect(screen.getAllByText('Overview').length).toBeGreaterThan(0) + expect(screen.getAllByText('Repositories').length).toBeGreaterThan(0) + expect(screen.getAllByText('Contributors').length).toBeGreaterThan(0) + + // Check action buttons within mobile menu + expect(screen.getAllByRole('button', { name: /settings/i }).length).toBeGreaterThan(0) + expect(screen.getAllByRole('button', { name: /support us/i }).length).toBeGreaterThan(0) + }) + + it('closes mobile menu when Escape key is pressed', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + expect(hamburger).toHaveAttribute('aria-expanded', 'true') + + fireEvent.keyDown(document, { key: 'Escape' }) + + expect(hamburger).toHaveAttribute('aria-expanded', 'false') + expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() + expect(screen.queryByTestId('navbar-backdrop')).not.toBeInTheDocument() + }) + + it('closes mobile menu when clicking the backdrop overlay', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + const backdrop = screen.getByTestId('navbar-backdrop') + expect(backdrop).toBeInTheDocument() + + await userEvent.click(backdrop) + + expect(hamburger).toHaveAttribute('aria-expanded', 'false') + expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() + expect(screen.queryByTestId('navbar-backdrop')).not.toBeInTheDocument() + }) + + it('closes mobile menu when clicking a navigation link', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + expect(document.getElementById('mobile-nav-menu')).toBeInTheDocument() + + const mobileOverviewLink = screen.getAllByRole('link', { name: /overview/i }).find( + link => document.getElementById('mobile-nav-menu')?.contains(link) + ) + expect(mobileOverviewLink).toBeDefined() + + await userEvent.click(mobileOverviewLink) + + expect(hamburger).toHaveAttribute('aria-expanded', 'false') + expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() + }) + + it('closes mobile menu when clicking settings or support button', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + const mobileSettingsBtn = screen.getAllByRole('button', { name: /settings/i }).find( + btn => document.getElementById('mobile-nav-menu')?.contains(btn) + ) + expect(mobileSettingsBtn).toBeDefined() + + await userEvent.click(mobileSettingsBtn) + + expect(hamburger).toHaveAttribute('aria-expanded', 'false') + expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/RateLimitBanner.jsx b/src/components/RateLimitBanner.jsx index b09369da..999aafd8 100644 --- a/src/components/RateLimitBanner.jsx +++ b/src/components/RateLimitBanner.jsx @@ -20,6 +20,8 @@ export default function RateLimitBanner() { background: crit ? 'rgba(239,68,68,.07)' : 'rgba(245,197,24,.06)', padding: '9px 24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', + flexWrap: 'wrap', + gap: '8px 16px', }}> From 392d38ffd4512d66384549deefb784aa67dca063 Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 18:34:31 +0530 Subject: [PATCH 05/11] fix: address mobile navigation review feedback --- src/App.jsx | 3 +- src/components/Navbar.jsx | 6 ++- src/components/Navbar.test.jsx | 69 ++++++++++++++++++++++-------- src/components/RateLimitBanner.jsx | 2 +- src/components/ThemeToggle.jsx | 9 +++- src/styles/global.css | 7 +++ 6 files changed, 70 insertions(+), 26 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 15576553..af1309a0 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -25,11 +25,10 @@ function Layout({ children }) { flexDirection: 'column', width: '100%', maxWidth: '100vw', - overflowX: 'hidden', }}> -
+
{children}
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 702d7e14..77cd99ab 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -6,6 +6,8 @@ import ThemeToggle from './ThemeToggle' import Logo from "../assets/og-logo.svg?react"; import { useTheme } from '../context/ThemeContext' +const THEME_LABEL = 'Theme' + const LINKS = [ { to: '/overview', label: 'Overview' }, { to: '/repositories', label: 'Repositories' }, @@ -165,8 +167,8 @@ export default function Navbar() { borderTop: hasData ? '1px solid var(--border)' : 'none', }}>
- Theme - + {THEME_LABEL} +
{rateLimit && ( diff --git a/src/components/Navbar.test.jsx b/src/components/Navbar.test.jsx index 1e193739..6618a9fc 100644 --- a/src/components/Navbar.test.jsx +++ b/src/components/Navbar.test.jsx @@ -1,6 +1,6 @@ import React from 'react' import { describe, it, expect, beforeEach, vi } from 'vitest' -import { render, screen, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter } from 'react-router-dom' import Navbar from './Navbar' @@ -35,6 +35,7 @@ describe('Navbar Mobile Navigation', () => { beforeEach(() => { mockAppState.orgs = [{ login: 'AOSSIE-Org' }] mockAppState.rateLimit = { remaining: 50, limit: 60 } + mockThemeState.toggleTheme.mockClear() }) it('renders hamburger button with initial aria-expanded="false"', () => { @@ -67,14 +68,17 @@ describe('Navbar Mobile Navigation', () => { const mobileMenu = document.getElementById('mobile-nav-menu') expect(mobileMenu).toBeInTheDocument() - // Check links within mobile menu - expect(screen.getAllByText('Overview').length).toBeGreaterThan(0) - expect(screen.getAllByText('Repositories').length).toBeGreaterThan(0) - expect(screen.getAllByText('Contributors').length).toBeGreaterThan(0) + const menu = within(mobileMenu) - // Check action buttons within mobile menu - expect(screen.getAllByRole('button', { name: /settings/i }).length).toBeGreaterThan(0) - expect(screen.getAllByRole('button', { name: /support us/i }).length).toBeGreaterThan(0) + // Check links scoped to mobile menu + expect(menu.getByRole('link', { name: /overview/i })).toBeInTheDocument() + expect(menu.getByRole('link', { name: /repositories/i })).toBeInTheDocument() + expect(menu.getByRole('link', { name: /contributors/i })).toBeInTheDocument() + + // Check action controls scoped to mobile menu + expect(menu.getByText('Theme')).toBeInTheDocument() + expect(menu.getByRole('button', { name: /settings/i })).toBeInTheDocument() + expect(menu.getByRole('button', { name: /support us/i })).toBeInTheDocument() }) it('closes mobile menu when Escape key is pressed', async () => { @@ -111,32 +115,59 @@ describe('Navbar Mobile Navigation', () => { const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) await userEvent.click(hamburger) - expect(document.getElementById('mobile-nav-menu')).toBeInTheDocument() - - const mobileOverviewLink = screen.getAllByRole('link', { name: /overview/i }).find( - link => document.getElementById('mobile-nav-menu')?.contains(link) - ) - expect(mobileOverviewLink).toBeDefined() + const mobileMenu = document.getElementById('mobile-nav-menu') + expect(mobileMenu).toBeInTheDocument() + const mobileOverviewLink = within(mobileMenu).getByRole('link', { name: /overview/i }) await userEvent.click(mobileOverviewLink) expect(hamburger).toHaveAttribute('aria-expanded', 'false') expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() }) - it('closes mobile menu when clicking settings or support button', async () => { + it('closes mobile menu when clicking settings button', async () => { renderNavbar() const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) await userEvent.click(hamburger) - const mobileSettingsBtn = screen.getAllByRole('button', { name: /settings/i }).find( - btn => document.getElementById('mobile-nav-menu')?.contains(btn) - ) - expect(mobileSettingsBtn).toBeDefined() + const mobileMenu = document.getElementById('mobile-nav-menu') + expect(mobileMenu).toBeInTheDocument() + const mobileSettingsBtn = within(mobileMenu).getByRole('button', { name: /settings/i }) await userEvent.click(mobileSettingsBtn) expect(hamburger).toHaveAttribute('aria-expanded', 'false') expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() }) + + it('closes mobile menu when clicking support us button', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + const mobileMenu = document.getElementById('mobile-nav-menu') + expect(mobileMenu).toBeInTheDocument() + + const mobileSupportBtn = within(mobileMenu).getByRole('button', { name: /support us/i }) + await userEvent.click(mobileSupportBtn) + + expect(hamburger).toHaveAttribute('aria-expanded', 'false') + expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() + }) + + it('closes mobile menu when clicking theme toggle button and calls toggleTheme', async () => { + renderNavbar() + const hamburger = screen.getByRole('button', { name: /toggle navigation menu/i }) + + await userEvent.click(hamburger) + const mobileMenu = document.getElementById('mobile-nav-menu') + expect(mobileMenu).toBeInTheDocument() + + const themeToggleBtn = within(mobileMenu).getByRole('button', { name: /switch to/i }) + await userEvent.click(themeToggleBtn) + + expect(mockThemeState.toggleTheme).toHaveBeenCalled() + expect(hamburger).toHaveAttribute('aria-expanded', 'false') + expect(document.getElementById('mobile-nav-menu')).not.toBeInTheDocument() + }) }) diff --git a/src/components/RateLimitBanner.jsx b/src/components/RateLimitBanner.jsx index 999aafd8..5193a12a 100644 --- a/src/components/RateLimitBanner.jsx +++ b/src/components/RateLimitBanner.jsx @@ -23,7 +23,7 @@ export default function RateLimitBanner() { flexWrap: 'wrap', gap: '8px 16px', }}> - + API RATE LIMIT: {rateLimit.remaining} / {rateLimit.limit} REQUESTS REMAINING {!pat && ( diff --git a/src/components/ThemeToggle.jsx b/src/components/ThemeToggle.jsx index fa74fc8d..9bdf2119 100644 --- a/src/components/ThemeToggle.jsx +++ b/src/components/ThemeToggle.jsx @@ -1,12 +1,17 @@ import { FiSun, FiMoon } from "react-icons/fi"; import { useTheme } from "../context/ThemeContext"; -export default function ThemeToggle() { +export default function ThemeToggle({ onToggle }) { const { theme, toggleTheme } = useTheme(); + const handleClick = (e) => { + toggleTheme(e); + onToggle?.(e); + }; + return (
@@ -124,7 +128,7 @@ export default function Navbar() { ref={hamburgerRef} className="navbar-hamburger" onClick={() => setMenuOpen(prev => !prev)} - aria-label="Toggle navigation menu" + aria-label={HAMBURGER_LABEL} aria-expanded={menuOpen} aria-controls="mobile-nav-menu" style={{ @@ -185,7 +189,7 @@ export default function Navbar() { {rateLimit && (
- {rateLimit.remaining.toLocaleString()} / {rateLimit.limit.toLocaleString()} API calls remaining + {rateLimit.remaining.toLocaleString()} / {rateLimit.limit.toLocaleString()} {API_CALLS_REMAINING_LABEL}
)} @@ -205,7 +209,7 @@ export default function Navbar() { width: '100%', }} > - Settings + {SETTINGS_LABEL} From b8bc0ff596a7458a69a6f8e9a32292be19d6087b Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 18:55:36 +0530 Subject: [PATCH 08/11] fix: externalize navbar mobile menu strings --- src/components/Navbar.jsx | 23 ++++++++--------------- src/constants/navbar.js | 22 ++++++++++++++++++++++ src/constants/strings.js | 1 + 3 files changed, 31 insertions(+), 15 deletions(-) create mode 100644 src/constants/navbar.js create mode 100644 src/constants/strings.js diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx index 7edfe84b..54b8554f 100644 --- a/src/components/Navbar.jsx +++ b/src/components/Navbar.jsx @@ -5,21 +5,14 @@ import { useApp } from '../context/AppContext' import ThemeToggle from './ThemeToggle' import Logo from "../assets/og-logo.svg?react"; import { useTheme } from '../context/ThemeContext' - -const THEME_LABEL = 'Theme' -const API_CALLS_REMAINING_LABEL = 'API calls remaining' -const SETTINGS_LABEL = 'Settings' -const SUPPORT_US_LABEL = 'Support Us' -const HAMBURGER_LABEL = 'Toggle navigation menu' - -const LINKS = [ - { to: '/overview', label: 'Overview' }, - { to: '/repositories', label: 'Repositories' }, - { to: '/contributors', label: 'Contributors' }, - { to: '/network', label: 'Network' }, - { to: '/analytics', label: 'Analytics' }, - { to: '/governance', label: 'Governance' }, -] +import { + THEME_LABEL, + API_CALLS_REMAINING_LABEL, + SETTINGS_LABEL, + SUPPORT_US_LABEL, + HAMBURGER_LABEL, + NAV_LINKS as LINKS, +} from '../constants/navbar' export default function Navbar() { const { orgs, rateLimit } = useApp() diff --git a/src/constants/navbar.js b/src/constants/navbar.js new file mode 100644 index 00000000..0ef74605 --- /dev/null +++ b/src/constants/navbar.js @@ -0,0 +1,22 @@ +export const THEME_LABEL = 'Theme' +export const API_CALLS_REMAINING_LABEL = 'API calls remaining' +export const SETTINGS_LABEL = 'Settings' +export const SUPPORT_US_LABEL = 'Support Us' +export const HAMBURGER_LABEL = 'Toggle navigation menu' + +export const NAV_LINKS = [ + { to: '/overview', label: 'Overview' }, + { to: '/repositories', label: 'Repositories' }, + { to: '/contributors', label: 'Contributors' }, + { to: '/network', label: 'Network' }, + { to: '/analytics', label: 'Analytics' }, + { to: '/governance', label: 'Governance' }, +] + +export const NAVBAR_STRINGS = { + THEME: THEME_LABEL, + API_CALLS_REMAINING: API_CALLS_REMAINING_LABEL, + SETTINGS: SETTINGS_LABEL, + SUPPORT_US: SUPPORT_US_LABEL, + HAMBURGER: HAMBURGER_LABEL, +} diff --git a/src/constants/strings.js b/src/constants/strings.js new file mode 100644 index 00000000..06c22c60 --- /dev/null +++ b/src/constants/strings.js @@ -0,0 +1 @@ +export * from './navbar' From 7a168c5b5f5400167e56638cc387e316141ed891 Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 19:35:28 +0530 Subject: [PATCH 09/11] fix(banner): allow rate limit content to shrink and wrap cleanly --- src/components/RateLimitBanner.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/RateLimitBanner.jsx b/src/components/RateLimitBanner.jsx index 5193a12a..8b33a908 100644 --- a/src/components/RateLimitBanner.jsx +++ b/src/components/RateLimitBanner.jsx @@ -23,13 +23,13 @@ export default function RateLimitBanner() { flexWrap: 'wrap', gap: '8px 16px', }}> - - + + API RATE LIMIT: {rateLimit.remaining} / {rateLimit.limit} REQUESTS REMAINING {!pat && ( navigate('/settings')} - style={{ color: 'var(--accent)', marginLeft: 10, cursor: 'pointer', fontWeight: 600 }} + style={{ color: 'var(--accent)', cursor: 'pointer', fontWeight: 600 }} > Add PAT for 5,000 req/hr From 7666cd1353d95510c5f6bae3f32c07f849096573 Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 19:43:17 +0530 Subject: [PATCH 10/11] fix(banner): make Add PAT action semantic Link and add unit tests --- src/components/RateLimitBanner.jsx | 11 ++-- src/components/RateLimitBanner.test.jsx | 79 +++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 src/components/RateLimitBanner.test.jsx diff --git a/src/components/RateLimitBanner.jsx b/src/components/RateLimitBanner.jsx index 8b33a908..19190c93 100644 --- a/src/components/RateLimitBanner.jsx +++ b/src/components/RateLimitBanner.jsx @@ -1,11 +1,10 @@ import React from 'react' -import { useNavigate } from 'react-router-dom' +import { Link } from 'react-router-dom' import { FiZap, FiAlertTriangle } from 'react-icons/fi' import { useApp } from '../context/AppContext' export default function RateLimitBanner() { const { rateLimit, pat } = useApp() - const navigate = useNavigate() if (!rateLimit) return null const pct = rateLimit.remaining / rateLimit.limit @@ -27,12 +26,12 @@ export default function RateLimitBanner() { API RATE LIMIT: {rateLimit.remaining} / {rateLimit.limit} REQUESTS REMAINING {!pat && ( - navigate('/settings')} - style={{ color: 'var(--accent)', cursor: 'pointer', fontWeight: 600 }} + Add PAT for 5,000 req/hr - + )} diff --git a/src/components/RateLimitBanner.test.jsx b/src/components/RateLimitBanner.test.jsx new file mode 100644 index 00000000..7c8128f0 --- /dev/null +++ b/src/components/RateLimitBanner.test.jsx @@ -0,0 +1,79 @@ +import React from 'react' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter } from 'react-router-dom' +import RateLimitBanner from './RateLimitBanner' + +const mockAppState = { + rateLimit: { remaining: 10, limit: 60, used: 50, reset: 1700000000 }, + pat: null, +} + +vi.mock('../context/AppContext', () => ({ + useApp: () => mockAppState, +})) + +function renderRateLimitBanner() { + return render( + + + + ) +} + +describe('RateLimitBanner', () => { + beforeEach(() => { + mockAppState.rateLimit = { remaining: 10, limit: 60, used: 50, reset: 1700000000 } + mockAppState.pat = null + }) + + it('renders nothing when rateLimit is null', () => { + mockAppState.rateLimit = null + const { container } = renderRateLimitBanner() + expect(container.firstChild).toBeNull() + }) + + it('renders nothing when rate limit is healthy (>20% and limit > 60)', () => { + mockAppState.rateLimit = { remaining: 5000, limit: 5000, used: 0, reset: 1700000000 } + const { container } = renderRateLimitBanner() + expect(container.firstChild).toBeNull() + }) + + it('renders banner when rate limit is low', () => { + renderRateLimitBanner() + expect(screen.getByText(/API RATE LIMIT:/i)).toBeInTheDocument() + expect(screen.getByText('10 / 60')).toBeInTheDocument() + }) + + it('renders "Add PAT" as a semantic Link pointing to /settings when pat is absent', () => { + renderRateLimitBanner() + const patLink = screen.getByRole('link', { name: /add pat for 5,000 req\/hr/i }) + expect(patLink).toBeInTheDocument() + expect(patLink).toHaveAttribute('href', '/settings') + }) + + it('allows keyboard focus navigation to the Add PAT link', async () => { + renderRateLimitBanner() + const patLink = screen.getByRole('link', { name: /add pat for 5,000 req\/hr/i }) + + expect(document.body).toHaveFocus() + await userEvent.tab() + expect(patLink).toHaveFocus() + }) + + it('does not render Add PAT link when pat is already provided', () => { + mockAppState.pat = 'ghp_testtoken' + renderRateLimitBanner() + expect(screen.queryByRole('link', { name: /add pat for 5,000 req\/hr/i })).not.toBeInTheDocument() + }) + + it('applies flex wrapping and minWidth: 0 to allow responsive shrinking without horizontal overflow', () => { + const { container } = renderRateLimitBanner() + const bannerContainer = container.firstChild + expect(bannerContainer).toHaveStyle({ display: 'flex', flexWrap: 'wrap' }) + + const messageSpan = bannerContainer.firstChild + expect(messageSpan).toHaveStyle({ display: 'flex', flexWrap: 'wrap', minWidth: '0px' }) + }) +}) From c326968f22bf5a73731dd35af12822647b6d42c2 Mon Sep 17 00:00:00 2001 From: Sagar Gupta Date: Fri, 2 Oct 2026 19:45:26 +0530 Subject: [PATCH 11/11] fix(banner): wrap rate limit text in span for clean multi-line wrapping --- src/components/RateLimitBanner.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/RateLimitBanner.jsx b/src/components/RateLimitBanner.jsx index 19190c93..7dd523fb 100644 --- a/src/components/RateLimitBanner.jsx +++ b/src/components/RateLimitBanner.jsx @@ -24,7 +24,9 @@ export default function RateLimitBanner() { }}> - API RATE LIMIT: {rateLimit.remaining} / {rateLimit.limit} REQUESTS REMAINING + + API RATE LIMIT: {rateLimit.remaining} / {rateLimit.limit} REQUESTS REMAINING + {!pat && (