diff --git a/CHANGELOG.md b/CHANGELOG.md index 0165a000..3a4b931a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Web header — restore search icon on non-home pages (2026-05-24) + +- **Search icon back in the header** ([`4eb1986`](https://github.com/mrviduus/textstack/commit/4eb1986)) — was removed in [`3e53e3e`](https://github.com/mrviduus/textstack/commit/3e53e3e) on the assumption that the hero search on home was enough. It wasn't: on every other page (library, discover, vocabulary, reader, …) the hero is gone and users had nowhere to launch a search from. Icon now shows on all routes except home (where the hero input still owns the affordance), opens the existing `MobileSearchOverlay`, and ships with 10 new `Header.test.tsx` cases pinning visibility per route + open/close flow. + ### Safe refactor — backend partial-class splits + util tests (2026-05-24) Cosmetic refactor pass: three god-class backend files split across C# partial diff --git a/apps/web/src/components/Header.tsx b/apps/web/src/components/Header.tsx index 2fa6302f..7326f5d7 100644 --- a/apps/web/src/components/Header.tsx +++ b/apps/web/src/components/Header.tsx @@ -1,6 +1,8 @@ import { useState, useRef } from 'react' +import { useLocation } from 'react-router-dom' import { LocalizedLink } from './LocalizedLink' import { DiscoverMenu } from './DiscoverMenu' +import { MobileSearchOverlay } from './Search' import { LoginButton } from './auth/LoginButton' import { UserMenu } from './auth/UserMenu' import { useAuth } from '../context/AuthContext' @@ -15,12 +17,20 @@ import { emit } from '../lib/telemetry/navTelemetry' export function Header() { const [badgePopup, setBadgePopup] = useState(false) + const [searchOpen, setSearchOpen] = useState(false) const badgeWrapperRef = useRef(null) const { isAuthenticated, isLoading } = useAuth() const isScrolled = useScrolled(50) const { isDark, toggleTheme } = useDarkMode() const { t } = useTranslation() const quickStats = useQuickStats() + // Suppress the header search icon on the home page — HeroSection already + // renders a prominent search input there, so duplicating it in the chrome + // would be visual noise. On every other route the hero is gone, and users + // have nowhere else to launch a search from (was the regression that + // motivated bringing the icon back — see 3e53e3e for the removal). + const location = useLocation() + const isHomePage = /^\/(en|uk)?\/?$/.test(location.pathname) return (
@@ -68,6 +78,19 @@ export function Header() {
+ {!isHomePage && ( + + )}
+ {searchOpen && setSearchOpen(false)} />}
) } diff --git a/apps/web/src/components/__tests__/Header.test.tsx b/apps/web/src/components/__tests__/Header.test.tsx index b1a92bc4..30030668 100644 --- a/apps/web/src/components/__tests__/Header.test.tsx +++ b/apps/web/src/components/__tests__/Header.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' -import { render, screen } from '@testing-library/react' +import { render, screen, fireEvent } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { Header } from '../Header' @@ -25,6 +25,7 @@ vi.mock('../../hooks/useTranslation', () => ({ 'nav.library': 'Library', 'nav.discover': 'Discover', 'nav.vocabulary': 'Vocabulary', + 'nav.search': 'Search', 'nav.about': 'About', 'nav.aboutTextStack': 'About TextStack', 'nav.brandTitle': 'TextStack', @@ -40,10 +41,19 @@ vi.mock('../auth/UserMenu', () => ({ UserMenu: () =>
+ +
+ ), +})) -function renderHeader() { +function renderHeader(initialPath = '/') { return render( - +
) @@ -87,4 +97,65 @@ describe('Header', () => { const brand = screen.getByTitle('TextStack') expect(brand).toHaveAttribute('href', '/en') }) + + // Search icon was removed in 3e53e3e ("clean header") on the assumption that + // the hero search on home was enough. It wasn't — on every other page the + // hero is gone and users had nowhere to launch a search from. These tests + // pin the new behavior: visible on non-home routes, hidden on home + on /uk + // home (and bare home for unauth marketing root). + + it('search icon hidden on home (hero already has search)', () => { + renderHeader('/') + expect(screen.queryByLabelText('Search')).not.toBeInTheDocument() + }) + + it('search icon hidden on /en home', () => { + renderHeader('/en') + expect(screen.queryByLabelText('Search')).not.toBeInTheDocument() + }) + + it('search icon hidden on /en/ trailing-slash home', () => { + renderHeader('/en/') + expect(screen.queryByLabelText('Search')).not.toBeInTheDocument() + }) + + it('search icon hidden on /uk home (multilingual root)', () => { + renderHeader('/uk') + expect(screen.queryByLabelText('Search')).not.toBeInTheDocument() + }) + + it('search icon visible on /en/library', () => { + renderHeader('/en/library') + expect(screen.getByLabelText('Search')).toBeInTheDocument() + }) + + it('search icon visible on /en/discover', () => { + renderHeader('/en/discover') + expect(screen.getByLabelText('Search')).toBeInTheDocument() + }) + + it('search icon visible on /en/vocabulary', () => { + renderHeader('/en/vocabulary') + expect(screen.getByLabelText('Search')).toBeInTheDocument() + }) + + it('search icon visible on a reader route /en/books/foo', () => { + renderHeader('/en/books/foo') + expect(screen.getByLabelText('Search')).toBeInTheDocument() + }) + + it('clicking search icon opens overlay', () => { + renderHeader('/en/library') + expect(screen.queryByTestId('search-overlay')).not.toBeInTheDocument() + fireEvent.click(screen.getByLabelText('Search')) + expect(screen.getByTestId('search-overlay')).toBeInTheDocument() + }) + + it('overlay close button removes the overlay', () => { + renderHeader('/en/library') + fireEvent.click(screen.getByLabelText('Search')) + expect(screen.getByTestId('search-overlay')).toBeInTheDocument() + fireEvent.click(screen.getByText('Close search')) + expect(screen.queryByTestId('search-overlay')).not.toBeInTheDocument() + }) }) diff --git a/apps/web/src/locales/en.json b/apps/web/src/locales/en.json index 9fe336b9..18dbb12e 100644 --- a/apps/web/src/locales/en.json +++ b/apps/web/src/locales/en.json @@ -280,6 +280,7 @@ "library": "Library", "discover": "Discover", "vocabulary": "Vocabulary", + "search": "Search", "highlights": "Highlights", "about": "About", "brandTitle": "TextStack Reader - Learn languages through reading",