From 8f95d143ab2f900f3a3fa0e1195a0678f8858146 Mon Sep 17 00:00:00 2001 From: smog123 Date: Mon, 31 Aug 2026 11:50:48 +0100 Subject: [PATCH] feat(404): add helpful NotFound page with search and analytics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a custom 404 page with a clear "Page not found" heading, descriptive subheading, a search input for searching keys directly from the 404 page, a Back to Marketplace button, and telemetry logging of the attempted path. Refs: #863 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- src/pages/NotFoundPage.tsx | 69 ++++++++++++++++--- .../NotFoundPage.integration.test.tsx | 30 ++++++-- src/pages/__tests__/NotFoundPage.test.tsx | 58 ++++++++++++++-- 3 files changed, 137 insertions(+), 20 deletions(-) diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx index 3cbce28e..672be7f5 100644 --- a/src/pages/NotFoundPage.tsx +++ b/src/pages/NotFoundPage.tsx @@ -1,8 +1,33 @@ -import { Link } from 'react-router'; -import { ArrowLeft, Compass } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Link, useLocation, useNavigate } from 'react-router'; +import { ArrowLeft, Compass, Search } from 'lucide-react'; import { Button } from '@/components/ui/button'; +import { useTelemetry } from '@/hooks/useTelemetry'; function NotFoundPage() { + const location = useLocation(); + const navigate = useNavigate(); + const { track } = useTelemetry(); + const [searchQuery, setSearchQuery] = useState(''); + + // Log 404 event with the attempted path on mount + useEffect(() => { + track({ + type: 'UserAction', + action: '404_page_view', + target: 'not_found_page', + metadata: { path: location.pathname + location.search }, + }); + }, [track, location.pathname, location.search]); + + const handleSearchSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const trimmed = searchQuery.trim(); + if (trimmed) { + navigate(`/?search=${encodeURIComponent(trimmed)}`); + } + }; + return (
@@ -18,21 +43,49 @@ function NotFoundPage() {

Access Layer

- This marketplace path is not live yet. + Page not found

- The creator key route you opened does not exist. Return to the - marketplace home to browse creators, review key pricing, and keep - exploring. + The page you are looking for doesn't exist or has been moved. + Return to the marketplace or search for creators below.

+ {/* Search Keys input */} +
+
+
+
+ setSearchQuery(e.target.value)} + /> +
+ +
+
diff --git a/src/pages/__tests__/NotFoundPage.integration.test.tsx b/src/pages/__tests__/NotFoundPage.integration.test.tsx index 62a93a44..60936f97 100644 --- a/src/pages/__tests__/NotFoundPage.integration.test.tsx +++ b/src/pages/__tests__/NotFoundPage.integration.test.tsx @@ -1,8 +1,12 @@ import { render, screen } from '@testing-library/react'; import { createMemoryRouter, RouterProvider } from 'react-router'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { routes } from '@/routes'; +vi.mock('@/hooks/useTelemetry', () => ({ + useTelemetry: () => ({ track: vi.fn() }), +})); + describe('NotFoundPage Integration', () => { it('renders NotFoundPage when navigating to an unknown route', () => { const router = createMemoryRouter(routes, { @@ -11,14 +15,26 @@ describe('NotFoundPage Integration', () => { render(); - // Assert the NotFoundPage content is rendered + // Assert the NotFoundPage heading is rendered + expect( + screen.getByRole('heading', { name: /page not found/i }) + ).toBeInTheDocument(); + + // Assert the subheading is rendered expect( - screen.getByRole('heading', { - name: /this marketplace path is not live yet/i, - }) + screen.getByText( + /the page you are looking for doesn't exist or has been moved/i + ) ).toBeInTheDocument(); - // Assert the page title or heading contains a 404 or not found message - expect(screen.getByText(/route not found/i)).toBeInTheDocument(); + // Assert the Back to Marketplace button exists + expect( + screen.getByRole('link', { name: /back to marketplace/i }) + ).toHaveAttribute('href', '/'); + + // Assert the search input exists + expect( + screen.getByRole('textbox', { name: /search keys/i }) + ).toBeInTheDocument(); }); }); diff --git a/src/pages/__tests__/NotFoundPage.test.tsx b/src/pages/__tests__/NotFoundPage.test.tsx index 7749e68d..58e1cde6 100644 --- a/src/pages/__tests__/NotFoundPage.test.tsx +++ b/src/pages/__tests__/NotFoundPage.test.tsx @@ -1,10 +1,15 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter } from 'react-router'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import NotFoundPage from '@/pages/NotFoundPage'; +vi.mock('@/hooks/useTelemetry', () => ({ + useTelemetry: () => ({ track: vi.fn() }), +})); + describe('NotFoundPage', () => { - it('points unknown routes back to the marketplace home', () => { + it('renders the Page not found heading and subheading', () => { render( @@ -12,13 +17,56 @@ describe('NotFoundPage', () => { ); expect( - screen.getByRole('heading', { - name: /this marketplace path is not live yet/i, - }) + screen.getByRole('heading', { name: /page not found/i }) + ).toBeInTheDocument(); + + expect( + screen.getByText( + /the page you are looking for doesn't exist or has been moved/i + ) ).toBeInTheDocument(); + }); + + it('has a Back to Marketplace link pointing to the root route', () => { + render( + + + + ); expect( screen.getByRole('link', { name: /back to marketplace/i }) ).toHaveAttribute('href', '/'); }); + + it('renders a search input for searching creators', () => { + render( + + + + ); + + expect( + screen.getByRole('textbox', { name: /search keys/i }) + ).toBeInTheDocument(); + }); + + it('navigates to root with search param on search submit', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const searchInput = screen.getByRole('textbox', { + name: /search keys/i, + }); + await user.type(searchInput, 'Alex'); + await user.click(screen.getByRole('button', { name: /search/i })); + + // After navigation the URL should contain the search query + expect(searchInput).toHaveValue('Alex'); + }); });