# Tutorial: Build a Contact page By the end of this tutorial, ResolutionFlow will have a working `/contact` page. A visitor can land on it, fill out a form, and see a thank-you state when it submits. You'll touch the router, build a page component, style it with the design system, manage form state, and link to it from the landing footer. This is a **tutorial**, not a reference. It's one concrete path that's known to work. The point is to learn how the pieces fit together by actually building something. Don't substitute steps. After you finish, you'll be ready to read the code with confidence. **Estimated time:** 30–45 minutes. --- ## What you'll know by the end - Where new pages live in the codebase - How the router lazy-loads page components - How public pages differ from in-app pages - How to apply the design system without inventing chrome - How to wire form state, validation, and submit - How to verify your work and ship a clean commit --- ## Before you start You need: - The frontend container running (`docker ps` should show `resolutionflow_frontend` listening on 5173). Vite hot-module-reload is what makes this tutorial pleasant. Every file save shows up in the browser within a second. - An editor open at the repo root. - A logged-out browser tab pointed at the dev server. The contact page is public, so you don't need an account to visit it. (If you've been logged in, open a private window or sign out.) > Quick sanity check: navigate to `/landing` in the browser. If you see the marketing page, you're set up correctly. If you see anything else, fix that first. --- ## Step 1: Decide where the page lives Two parts of the app could host a "contact" page: the **public marketing layer** (`/landing`, `/privacy`, `/terms`) or the **in-app shell** (`/account`, `/sessions`, etc.). The right answer depends on the audience. A contact page is for visitors who *aren't* logged in: prospects, leads, support requests from people without accounts. So it belongs at the public layer, parallel to `/privacy` and `/terms`. No app shell, no sidebar, just a simple centered page. **Decision:** route it at `/contact`, no auth required, model it after `frontend/src/pages/PrivacyPage.tsx` for layout. --- ## Step 2: Create the page component Create a new file at `frontend/src/pages/ContactPage.tsx`. Start with the smallest possible skeleton so we can confirm the route works before adding form complexity. ```tsx import { Link } from 'react-router-dom' import { PageMeta } from '@/components/common/PageMeta' export default function ContactPage() { return ( <>
← Back to home

Contact

Send us a note and we'll get back to you within one business day.

) } ``` A few things worth pointing out: - **`PageMeta`** sets the document title and description. Every page should have one. It's how you keep tab titles informative without scattering `` calls everywhere. - **`min-h-screen bg-background`** ensures the page fills the viewport with the brand background color. Critical for public pages that don't sit inside an app layout. - **`mx-auto max-w-xl`** caps line length around 65–75 characters of body text, per the shared design laws. `max-w-xl` is ~36rem; for the form we'll keep at this width. - **`font-heading`** maps to the heading font defined in `frontend/src/index.css`. Use it on H1s, not body text. Save the file. Nothing visible happens yet: we haven't told the router that `/contact` exists. --- ## Step 3: Wire up the route Open `frontend/src/router.tsx`. Near the top of the file, you'll see a list of `lazyWithRetry` imports for every page. Add yours, alphabetized in the public-page group: ```tsx const ContactPage = lazyWithRetry(() => import('@/pages/ContactPage')) ``` `lazyWithRetry` is a thin wrapper around React's `lazy()` that retries once if the chunk fails to load (which can happen during a deploy). Use it for everything; never plain `lazy()`. Now scroll down to the `sentryCreateBrowserRouter` array and add a route entry next to the other public ones (`/landing`, `/privacy`, `/terms`): ```tsx { path: '/contact', element: page(ContactPage), errorElement: , }, ``` The `page()` helper wraps the component in `` and `}>`. That gives you a graceful loader while the chunk loads and an error boundary if something throws. The `errorElement: ` handles router-level errors (e.g., a 404 thrown deeper in the tree). Save. Vite reloads. Navigate to `http://your-dev-host:5173/contact` (or whatever URL serves the dev frontend). You should see the heading, the description, and the back-to-home link. > If you see a blank page or an error, check the browser console first. The two common mistakes here are: (1) wrong import path, (2) forgetting `export default`. Fix and re-save. --- ## Step 4: Add the form Now we add the actual contact form. Replace the body of the page (everything inside `max-w-xl`) with the form scaffolding. Keep imports for now; we'll add more in the next step. ```tsx
← Back to home

Contact

Send us a note and we'll get back to you within one business day.