Skip to content
Open
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
26 changes: 26 additions & 0 deletions src/components/NotFoundPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* @vitest-environment jsdom */

import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import { NotFoundPage } from "./NotFoundPage";

vi.mock("@tanstack/react-router", () => ({
Link: ({ children, to, className }: { children: ReactNode; to: string; className?: string }) => (
<a href={to} className={className}>
{children}
</a>
),
}));

describe("NotFoundPage", () => {
it("renders a single recovery action back to the homepage", () => {
render(<NotFoundPage />);

expect(screen.getByText("404 • Page not found")).toBeTruthy();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant .toBeTruthy() after getByText

screen.getByText(...) already throws (and fails the test) when the element is not found — the chained .toBeTruthy() is always truthy on a successful query and adds no extra safety net. Prefer toBeInTheDocument() for a more expressive assertion, or simply omit the assertion and rely on getByText's implicit throw:

Suggested change
expect(screen.getByText("404 • Page not found")).toBeTruthy();
expect(screen.getByText("404 • Page not found")).toBeInTheDocument();
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/components/NotFoundPage.test.tsx
Line: 20

Comment:
**Redundant `.toBeTruthy()` after `getByText`**

`screen.getByText(...)` already throws (and fails the test) when the element is not found — the chained `.toBeTruthy()` is always truthy on a successful query and adds no extra safety net. Prefer `toBeInTheDocument()` for a more expressive assertion, or simply omit the assertion and rely on `getByText`'s implicit throw:

```suggestion
    expect(screen.getByText("404 • Page not found")).toBeInTheDocument();
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

expect(screen.getByRole("link", { name: "Return home" }).getAttribute("href")).toBe("/");
expect(screen.queryByText("Lost route")).toBeNull();
expect(screen.queryByText("Try next")).toBeNull();
expect(screen.queryByRole("link", { name: /Browse / })).toBeNull();
});
});
23 changes: 23 additions & 0 deletions src/components/NotFoundPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Link } from "@tanstack/react-router";

export function NotFoundPage() {
return (
<main className="section not-found-page">
<section className="card not-found-card">
<span className="not-found-eyebrow">404 • Page not found</span>
<div className="not-found-copy">
<h1 className="section-title not-found-title">This page drifted out of reach.</h1>
<p className="section-subtitle not-found-description">
The link may be outdated, the URL may have a typo, or this page may have moved.
Let&apos;s get you back to something useful.
</p>
<div className="not-found-actions">
<Link to="/" className="btn btn-primary">
Return home
</Link>
</div>
</div>
</section>
</main>
);
}
17 changes: 17 additions & 0 deletions src/router.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { beforeAll, describe, expect, it } from "vitest";
import { NotFoundPage } from "./components/NotFoundPage";

describe("getRouter", () => {
beforeAll(() => {
process.env.VITE_CONVEX_URL = "https://example.convex.cloud";
process.env.VITE_CONVEX_SITE_URL = "https://example.convex.site";
process.env.SITE_URL = "http://localhost:3000";
});

it("registers the shared not found component", async () => {
const { getRouter } = await import("./router");
const router = getRouter();

expect(router.options.defaultNotFoundComponent).toBe(NotFoundPage);
});
});
2 changes: 2 additions & 0 deletions src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createRouter } from "@tanstack/react-router";
import { NotFoundPage } from "./components/NotFoundPage";
// Import the generated route tree
import { routeTree } from "./routeTree.gen";

Expand All @@ -10,6 +11,7 @@ export const getRouter = () => {

scrollRestoration: true,
defaultPreloadStaleTime: 0,
defaultNotFoundComponent: NotFoundPage,
});

return router;
Expand Down
70 changes: 70 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,76 @@ code {
box-sizing: border-box;
}

.not-found-page {
display: flex;
align-items: center;
min-height: clamp(420px, 62vh, 640px);
}

.not-found-card {
position: relative;
overflow: hidden;
gap: 20px;
padding: clamp(28px, 4vw, 42px);
background:
radial-gradient(circle at top right, rgba(255, 118, 84, 0.16), transparent 36%),
radial-gradient(circle at bottom left, rgba(32, 158, 146, 0.12), transparent 38%),
linear-gradient(180deg, color-mix(in srgb, var(--surface) 96%, white 4%), var(--surface));
box-shadow: var(--shadow);
}

.not-found-eyebrow {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
border: 1px solid rgba(255, 118, 84, 0.24);
background: rgba(255, 118, 84, 0.1);
color: var(--accent-deep);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}

.not-found-copy {
display: grid;
gap: 14px;
max-width: 58ch;
}

.not-found-title {
margin: 0;
max-width: 12ch;
font-size: clamp(2.4rem, 5vw, 3.6rem);
line-height: 0.95;
}

.not-found-description {
margin: 0;
font-size: 1rem;
line-height: 1.7;
}

.not-found-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
}

@media (max-width: 760px) {
.not-found-title {
max-width: none;
}

.not-found-actions .btn {
width: 100%;
justify-content: center;
}
}

.upload-shell {
position: relative;
}
Expand Down