Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 61 additions & 8 deletions src/pages/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="min-h-screen overflow-hidden bg-[#06111f] text-white">
<section className="relative flex min-h-screen items-center px-6 py-16">
Expand All @@ -18,29 +43,57 @@ function NotFoundPage() {
<div className="relative mx-auto w-full max-w-4xl">
<div className="mb-8 inline-flex items-center gap-2 rounded-full border border-amber-300/20 bg-amber-300/10 px-4 py-2 font-jakarta text-sm font-bold text-amber-100">
<Compass className="size-4" aria-hidden="true" />
Route not found
Page not found
</div>

<p className="font-jakarta text-sm font-black uppercase tracking-[0.28em] text-amber-200/80">
Access Layer
</p>
<h1 className="mt-4 max-w-3xl font-grotesque text-5xl font-black leading-none tracking-tight sm:text-6xl md:text-7xl">
This marketplace path is not live yet.
Page not found
</h1>
<p className="mt-6 max-w-2xl font-jakarta text-base leading-8 text-white/70 sm:text-lg">
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.
</p>

{/* Search Keys input */}
<form
onSubmit={handleSearchSubmit}
className="mt-8 max-w-md"
role="search"
aria-label="Search creators from 404 page"
>
<div className="relative">
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<Search className="size-5 text-white/50" aria-hidden="true" />
</div>
<input
type="text"
aria-label="Search keys"
className="block w-full rounded-xl border border-white/10 bg-white/5 py-3 pl-10 pr-10 text-sm text-white placeholder:text-white/40 focus:border-amber-500/50 focus:bg-white/10 focus:outline-none focus:ring-2 focus:ring-amber-500/20"
placeholder="Search keys by creator name..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
/>
</div>
<Button
type="submit"
variant="outline"
className="mt-3 h-10 rounded-xl border-white/15 bg-white/5 px-5 font-jakarta text-sm font-bold text-white/80 hover:border-white/30 hover:bg-white/10 hover:text-white"
>
Search
</Button>
</form>

<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Button
asChild
className="h-12 rounded-xl bg-amber-400 px-5 font-jakarta font-black text-slate-950 hover:bg-amber-300"
>
<Link to="/">
<ArrowLeft className="size-4" aria-hidden="true" />
Back to marketplace
Back to Marketplace
</Link>
</Button>
</div>
Expand Down
30 changes: 23 additions & 7 deletions src/pages/__tests__/NotFoundPage.integration.test.tsx
Original file line number Diff line number Diff line change
@@ -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, {
Expand All @@ -11,14 +15,26 @@ describe('NotFoundPage Integration', () => {

render(<RouterProvider router={router} />);

// 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();
});
});
58 changes: 53 additions & 5 deletions src/pages/__tests__/NotFoundPage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,72 @@
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(
<MemoryRouter initialEntries={['/missing-route']}>
<NotFoundPage />
</MemoryRouter>
);

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(
<MemoryRouter initialEntries={['/missing-route']}>
<NotFoundPage />
</MemoryRouter>
);

expect(
screen.getByRole('link', { name: /back to marketplace/i })
).toHaveAttribute('href', '/');
});

it('renders a search input for searching creators', () => {
render(
<MemoryRouter initialEntries={['/missing-route']}>
<NotFoundPage />
</MemoryRouter>
);

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(
<MemoryRouter initialEntries={['/missing-route']}>
<NotFoundPage />
</MemoryRouter>
);

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');
});
});
Loading