From 4e8df006d75d8be2a1d8529ddf855c445854cba1 Mon Sep 17 00:00:00 2001 From: Tudor Date: Mon, 6 Jul 2026 17:01:45 +0100 Subject: [PATCH] feat(home): move "use my location" beside the hero search box The geolocation shortcut lived in the discovery strip below the results, away from the search. Move it directly under the hero search input, paired with the postcode hint, so the two ways to find nearby schools ("type a postcode" / "use my location") read as one idea and are visible at first glance. - FilterBar gains optional onNearMe/geoState/geoError props and renders the teal "Use my location" pill (with spinner + error) in hero mode; the geolocation flow itself still lives in HomeView. - Remove the now-duplicate near-me button and its dead CSS from the discovery section. - Refresh the search hint copy to pair with the button. - e2e: assert the "use my location" shortcut renders in the hero. Co-Authored-By: Claude Opus 4.8 --- e2e/tests/journeys.spec.ts | 10 +++ nextjs-app/components/FilterBar.module.css | 85 ++++++++++++++++++++++ nextjs-app/components/FilterBar.tsx | 60 ++++++++++++++- nextjs-app/components/HomeView.module.css | 62 ---------------- nextjs-app/components/HomeView.tsx | 32 +------- 5 files changed, 157 insertions(+), 92 deletions(-) diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 7c383d5..dbb8923 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -25,6 +25,16 @@ test('home page loads with hero search', async ({ page }) => { await expect(page.getByPlaceholder('School name or postcode').first()).toBeVisible(); }); +test('home hero offers a "use my location" shortcut beside the search box', async ({ page }) => { + await page.goto('/'); + // The geolocation shortcut lives inside the hero search card, right under the + // search input — not in a separate strip further down the page. + const searchInput = page.getByPlaceholder('School name or postcode').first(); + await expect(searchInput).toBeVisible(); + const nearMe = page.getByRole('button', { name: /use my location/i }); + await expect(nearMe).toBeVisible(); +}); + test('searching by name returns school results', async ({ page }) => { await searchByName(page, 'primary'); await expect(schoolLinks(page).first()).toBeVisible({ timeout: 15_000 }); diff --git a/nextjs-app/components/FilterBar.module.css b/nextjs-app/components/FilterBar.module.css index 485ba3a..ac74702 100644 --- a/nextjs-app/components/FilterBar.module.css +++ b/nextjs-app/components/FilterBar.module.css @@ -36,6 +36,91 @@ margin-bottom: 0; } +.searchHint { + margin: 0.875rem 0 0; + font-size: 0.95rem; + color: var(--text-secondary, #5a554d); + text-align: center; +} + +.searchHint strong { + color: var(--text-primary, #1a1612); + font-weight: 600; +} + +@media (max-width: 600px) { + .searchHint { + font-size: 0.85rem; + text-align: left; + } +} + +.nearMeRow { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.nearMeBtn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.375rem; + background: var(--accent-teal, #2d7d7d); + color: #fff; + border: none; + border-radius: 999px; + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease, transform 0.15s ease; + font-family: inherit; +} + +.nearMeBtn:hover:not(:disabled) { + background: #235f5f; + transform: translateY(-1px); +} + +.nearMeBtn:disabled { + opacity: 0.7; + cursor: not-allowed; +} + +.nearMeSpinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.35); + border-top-color: #fff; + border-radius: 50%; + animation: nearMeSpin 0.7s linear infinite; + flex-shrink: 0; +} + +@keyframes nearMeSpin { + to { + transform: rotate(360deg); + } +} + +.geoError { + font-size: 0.8125rem; + color: var(--accent-coral-dark, #b04a2e); + margin: 0; + max-width: 340px; + text-align: center; +} + +@media (max-width: 600px) { + .nearMeBtn { + width: 100%; + justify-content: center; + } +} + .searchSection { margin-bottom: 0; } diff --git a/nextjs-app/components/FilterBar.tsx b/nextjs-app/components/FilterBar.tsx index 4c7731f..51ee0eb 100644 --- a/nextjs-app/components/FilterBar.tsx +++ b/nextjs-app/components/FilterBar.tsx @@ -11,9 +11,21 @@ interface FilterBarProps { filters: Filters; isHero?: boolean; resultFilters?: ResultFilters; + // Geolocation "use my location" affordance, shown beside the hero search box. + // The state and handler live in HomeView (which owns the geolocation flow). + onNearMe?: () => void; + geoState?: "idle" | "requesting" | "error"; + geoError?: string | null; } -export function FilterBar({ filters, isHero, resultFilters }: FilterBarProps) { +export function FilterBar({ + filters, + isHero, + resultFilters, + onNearMe, + geoState = "idle", + geoError, +}: FilterBarProps) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); @@ -182,6 +194,52 @@ export function FilterBar({ filters, isHero, resultFilters }: FilterBarProps) { {isPending ?
: "Search"} + {isHero && ( + <> +

+ Search by school name — or use your{" "} + postcode for the nearest schools. +

+ {onNearMe && ( +
+ + {geoError && ( +

+ {geoError} +

+ )} +
+ )} + + )} {!isHero && ( diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css index 7b27fdc..81e6397 100644 --- a/nextjs-app/components/HomeView.module.css +++ b/nextjs-app/components/HomeView.module.css @@ -506,68 +506,6 @@ } } -.discoverySection { - padding: 0.5rem 0 0.5rem; - text-align: center; -} - -.nearMeRow { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - margin-bottom: 1.25rem; -} - -.nearMeBtn { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.625rem 1.375rem; - background: var(--accent-teal, #2d7d7d); - color: #fff; - border: none; - border-radius: 999px; - font-size: 0.9375rem; - font-weight: 600; - cursor: pointer; - transition: background 0.2s ease, transform 0.15s ease; - font-family: inherit; -} - -.nearMeBtn:hover:not(:disabled) { - background: #235f5f; - transform: translateY(-1px); -} - -.nearMeBtn:disabled { - opacity: 0.7; - cursor: not-allowed; -} - -.nearMeBtnSpinner { - display: inline-block; - width: 14px; - height: 14px; - border: 2px solid rgba(255, 255, 255, 0.35); - border-top-color: #fff; - border-radius: 50%; - animation: nearMeSpin 0.7s linear infinite; - flex-shrink: 0; -} - -@keyframes nearMeSpin { - to { transform: rotate(360deg); } -} - -.geoError { - font-size: 0.8125rem; - color: var(--accent-coral-dark, #b04a2e); - margin: 0; - max-width: 340px; - text-align: center; -} - .quickSearches { display: flex; align-items: center; diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx index f3bec73..945770b 100644 --- a/nextjs-app/components/HomeView.tsx +++ b/nextjs-app/components/HomeView.tsx @@ -284,37 +284,11 @@ export function HomeView({ initialSchools, filters, totalSchools, howItWorks, ed filters={filters} isHero={!isSearchActive} resultFilters={initialSchools.result_filters} + onNearMe={handleNearMe} + geoState={geoState} + geoError={geoError} /> - {/* Discovery section shown on landing page before any search */} - {!isSearchActive && initialSchools.schools.length === 0 && ( -
-
- - {geoError &&

{geoError}

} -
-
- )} - {/* Admissions countdown strip — only on landing page */} {!isSearchActive && (