From 4eb19862b0f72ffeddae1575d1d5deee2d9a0c95 Mon Sep 17 00:00:00 2001 From: Vasyl Vdovychenko Date: Sun, 24 May 2026 14:15:25 -0400 Subject: [PATCH 1/2] feat(web): restore header search icon on non-home pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 (library, discover, vocabulary, reader, etc) the hero is gone and users had nowhere to launch a search from. User report: 'не очевидно где его искать'. Re-add the search icon to Header.tsx, but only on non-home routes. Home keeps the prominent hero input (no duplicate UI in chrome). The icon opens the existing MobileSearchOverlay component (was orphaned in Search.tsx after the removal, now wired back up). - isHomePage check: /^\/(en|uk)?\/?$/ — covers /, /en, /en/, /uk, /uk/ - nav.search i18n key added (en.json) - Telemetry: emit('header.click', { item: 'search' }) - +10 Header tests covering visibility per route + open/close overlay flow - Total Header tests: 4 → 14; web suite 474 → 484 Tested manually via Playwright: - /en/ — no search icon in header (hero search present) - /en/books/ — search icon visible, clicking it opens overlay Co-Authored-By: Claude Opus 4.7 --- apps/web/src/components/Header.tsx | 24 ++++++ .../src/components/__tests__/Header.test.tsx | 77 ++++++++++++++++++- apps/web/src/locales/en.json | 1 + 3 files changed, 99 insertions(+), 3 deletions(-) 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", From 23f4924dd908258b7727db5a6ddc119bb1b9c71a Mon Sep 17 00:00:00 2001 From: Vasyl Vdovychenko Date: Sun, 24 May 2026 14:16:16 -0400 Subject: [PATCH 2/2] changelog: web header search icon restored on non-home pages Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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