From 0cff3902b2482f4d366a58afed06c61ce09ee64a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 15:34:50 +0000 Subject: [PATCH] feat(web): Product, How it works, Trust, Pricing and Docs pages, with a marketing e2e check Pricing shows no plans, prices or free trial until decided. Docs search and articles without pages are shown but not linked. Disables the agent file that next dev generates. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_014SdXCbrN5PtGExkpvMaMHx --- CHANGELOG.md | 2 +- e2e/package.json | 3 +- e2e/playwright.marketing.config.ts | 16 ++ e2e/tests/marketing.test.ts | 37 ++++ web/__tests__/marketing/pages.test.tsx | 188 ++++++++++++++++++++ web/__tests__/product/onboarding.test.tsx | 82 +++++++++ web/app/(marketing)/docs/page.tsx | 39 ++++ web/app/(marketing)/how-it-works/page.tsx | 77 ++++++++ web/app/(marketing)/page.tsx | 47 +++-- web/app/(marketing)/pricing/page.tsx | 13 ++ web/app/(marketing)/product/page.tsx | 63 +++++++ web/app/(marketing)/trust/page.tsx | 55 ++++++ web/next.config.ts | 2 + web/public/icons/camera.svg | 6 + web/public/icons/credit-card.svg | 5 + web/public/icons/help.svg | 6 + web/public/icons/key.svg | 7 + web/public/icons/plug.svg | 7 + web/public/icons/rocket.svg | 6 + web/public/icons/user.svg | 6 + web/public/icons/x.svg | 5 + web/src/marketing/Nav.tsx | 6 +- web/src/marketing/Shell.tsx | 14 ++ web/src/marketing/content.ts | 22 +++ web/src/marketing/home/PricingTeaser.tsx | 59 +++--- web/src/marketing/home/SafeByDesign.tsx | 29 +-- web/src/marketing/home/SectionHeader.tsx | 12 +- web/src/marketing/pages/AlwaysNever.tsx | 46 +++++ web/src/marketing/pages/ComparePlans.tsx | 41 +++++ web/src/marketing/pages/FeatureGrid.tsx | 26 +++ web/src/marketing/pages/FeatureRow.tsx | 38 ++++ web/src/marketing/pages/PageHero.tsx | 14 ++ web/src/marketing/pages/PopularArticles.tsx | 21 +++ web/src/marketing/pages/PricingContent.tsx | 44 +++++ web/src/marketing/pages/RowVisual.tsx | 26 +++ web/src/marketing/pages/setup-steps.ts | 16 ++ web/src/product/ActivityCard.tsx | 25 +++ web/src/product/Kbd.tsx | 6 + web/src/product/SearchField.tsx | 21 +++ web/src/product/SetupChecklist.tsx | 35 ++++ web/src/product/index.ts | 4 + web/src/styles/marketing.css | 53 ++++++ web/src/styles/product.css | 25 +++ web/src/ui/Icon.tsx | 8 +- 44 files changed, 1187 insertions(+), 76 deletions(-) create mode 100644 e2e/playwright.marketing.config.ts create mode 100644 e2e/tests/marketing.test.ts create mode 100644 web/__tests__/marketing/pages.test.tsx create mode 100644 web/__tests__/product/onboarding.test.tsx create mode 100644 web/app/(marketing)/docs/page.tsx create mode 100644 web/app/(marketing)/how-it-works/page.tsx create mode 100644 web/app/(marketing)/pricing/page.tsx create mode 100644 web/app/(marketing)/product/page.tsx create mode 100644 web/app/(marketing)/trust/page.tsx create mode 100644 web/public/icons/camera.svg create mode 100644 web/public/icons/credit-card.svg create mode 100644 web/public/icons/help.svg create mode 100644 web/public/icons/key.svg create mode 100644 web/public/icons/plug.svg create mode 100644 web/public/icons/rocket.svg create mode 100644 web/public/icons/user.svg create mode 100644 web/public/icons/x.svg create mode 100644 web/src/marketing/Shell.tsx create mode 100644 web/src/marketing/pages/AlwaysNever.tsx create mode 100644 web/src/marketing/pages/ComparePlans.tsx create mode 100644 web/src/marketing/pages/FeatureGrid.tsx create mode 100644 web/src/marketing/pages/FeatureRow.tsx create mode 100644 web/src/marketing/pages/PageHero.tsx create mode 100644 web/src/marketing/pages/PopularArticles.tsx create mode 100644 web/src/marketing/pages/PricingContent.tsx create mode 100644 web/src/marketing/pages/RowVisual.tsx create mode 100644 web/src/marketing/pages/setup-steps.ts create mode 100644 web/src/product/ActivityCard.tsx create mode 100644 web/src/product/Kbd.tsx create mode 100644 web/src/product/SearchField.tsx create mode 100644 web/src/product/SetupChecklist.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 27fc598..430bb02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The project is pre-1.0. Versions below `1.0.0` are internal milestones, not publ ### Added -- Web: Next.js app with design tokens (light and dark, AA contrast), the marketing Home page (desktop and phone), and a product component library built at design size (conversation, workspace, board, compare and approval) that the marketing mockups use and the app will reuse (ADR 0023). ADRs 0018 to 0022 (0022: hosting on DigitalOcean): Next.js web app (partly supersedes ADR 0011), sign-in by code, link and passkeys, Cloud provisioning on WeOwn Cloud, and keeping the "never sees a diff" rule wording. PRD, IMPLEMENTATION and AGENTS.md updated to match. +- Web: the five other marketing pages (Product, How it works, Trust, Pricing, Docs), a marketing e2e check (`pnpm --filter @wursor/e2e test:marketing`), and product components for setup, activity and search. Web: Next.js app with design tokens (light and dark, AA contrast), the marketing Home page (desktop and phone), and a product component library built at design size (conversation, workspace, board, compare and approval) that the marketing mockups use and the app will reuse (ADR 0023). ADRs 0018 to 0022 (0022: hosting on DigitalOcean): Next.js web app (partly supersedes ADR 0011), sign-in by code, link and passkeys, Cloud provisioning on WeOwn Cloud, and keeping the "never sees a diff" rule wording. PRD, IMPLEMENTATION and AGENTS.md updated to match. - `docs/prd-assets/headless-architecture.png`, the architecture diagram used in the PRD. - Design reference: Figma file "Wursor - Experiment" (Design system, Product Design, Marketing site and Brand Guideline pages). - **Monorepo workspace** — `pnpm` workspace (`api`, `web`, `e2e`), root `package.json` / `pnpm-workspace.yaml` / `tsconfig.base.json`, and a PHP `plugin` package (`composer.json`). Empty packages by design (see [ADR 8](docs/decisions/0008-empty-packages-not-stubs.md)). diff --git a/e2e/package.json b/e2e/package.json index d4d5473..6db014a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -6,7 +6,8 @@ "test": "vitest run", "golden": "tsx golden/src/run-golden.ts", "mirror:time": "tsx golden/src/run-mirror-timing.ts", - "agent:spike": "tsx agent/run-agent-spike.ts" + "agent:spike": "tsx agent/run-agent-spike.ts", + "test:marketing": "playwright test --config playwright.marketing.config.ts" }, "devDependencies": { "@playwright/test": "^1.62.1", diff --git a/e2e/playwright.marketing.config.ts b/e2e/playwright.marketing.config.ts new file mode 100644 index 0000000..7d1e296 --- /dev/null +++ b/e2e/playwright.marketing.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from '@playwright/test'; + +/** Marketing-site checks only: needs the web app, not the API. Run with `pnpm --filter @wursor/e2e test:marketing`. */ +export default defineConfig({ + testDir: './tests', + testMatch: 'marketing.test.ts', + fullyParallel: true, + retries: 0, + use: { baseURL: 'http://localhost:5173', launchOptions: { executablePath: process.env.PLAYWRIGHT_CHROMIUM_PATH ?? undefined } }, + webServer: { + command: 'pnpm --filter @wursor/web dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/e2e/tests/marketing.test.ts b/e2e/tests/marketing.test.ts new file mode 100644 index 0000000..5d21f7c --- /dev/null +++ b/e2e/tests/marketing.test.ts @@ -0,0 +1,37 @@ +import { expect, test } from '@playwright/test'; + +const PAGES = ['/', '/product', '/how-it-works', '/trust', '/pricing', '/docs']; + +for (const path of PAGES) { + test(`${path}: every Get started and Sign in goes to sign-up or sign-in (PX-13)`, async ({ page }) => { + await page.goto(path); + const links = page.getByRole('link', { name: /^(Get started|Sign in)$/ }); + const hrefs = await links.evaluateAll((els) => els.map((e) => e.getAttribute('href'))); + expect(hrefs.length).toBeGreaterThanOrEqual(2); + for (const href of hrefs) expect(['/signup', '/signin']).toContain(href); + }); + + test(`${path}: has one page heading and no horizontal scroll on a phone`, async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto(path); + await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + expect(overflow).toBeLessThanOrEqual(0); + }); +} + +test('the navigation reaches every marketing page', async ({ page }) => { + await page.goto('/'); + for (const [name, path] of [['Product', '/product'], ['How it works', '/how-it-works'], ['Trust', '/trust'], ['Pricing', '/pricing'], ['Docs', '/docs']] as const) { + await page.getByRole('navigation', { name: 'Primary' }).getByRole('link', { name }).click(); + await expect(page).toHaveURL(new RegExp(`${path}$`)); + await expect(page.getByRole('navigation', { name: 'Primary' }).getByRole('link', { name })).toHaveAttribute('aria-current', 'page'); + } +}); + +test('the mobile menu opens and lists every page', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/'); + await page.getByRole('button', { name: 'Open menu' }).click(); + await expect(page.getByRole('navigation', { name: 'Mobile' }).getByRole('link')).toHaveCount(6); +}); diff --git a/web/__tests__/marketing/pages.test.tsx b/web/__tests__/marketing/pages.test.tsx new file mode 100644 index 0000000..aca5569 --- /dev/null +++ b/web/__tests__/marketing/pages.test.tsx @@ -0,0 +1,188 @@ +import { render, screen, within } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import DocsPage from '../../app/(marketing)/docs/page.tsx'; +import HowItWorksPage from '../../app/(marketing)/how-it-works/page.tsx'; +import PricingPage from '../../app/(marketing)/pricing/page.tsx'; +import ProductPage from '../../app/(marketing)/product/page.tsx'; +import TrustPage from '../../app/(marketing)/trust/page.tsx'; +import { AlwaysNever } from '../../src/marketing/pages/AlwaysNever.tsx'; +import { ComparePlans } from '../../src/marketing/pages/ComparePlans.tsx'; +import { FeatureGrid } from '../../src/marketing/pages/FeatureGrid.tsx'; +import { FeatureRow } from '../../src/marketing/pages/FeatureRow.tsx'; +import { PageHero } from '../../src/marketing/pages/PageHero.tsx'; +import { PopularArticles } from '../../src/marketing/pages/PopularArticles.tsx'; +import { PricingContent } from '../../src/marketing/pages/PricingContent.tsx'; +import { Button } from '../../src/marketing/Button.tsx'; + +const PAGES = [ + ['Product', ProductPage, 'One workspace for every change.'], + ['How it works', HowItWorksPage, 'Four steps. You stay in charge.'], + ['Trust', TrustPage, 'Safe by design, not by promise.'], + ['Pricing', PricingPage, 'Simple plans. No surprises.'], + ['Docs', DocsPage, 'Find an answer fast.'], +] as const; + +describe.each(PAGES)('%s page', (_name, Page, heading) => { + it('has one page heading, the navigation and the footer', () => { + render(); + expect(screen.getAllByRole('heading', { level: 1 })).toHaveLength(1); + expect(screen.getByRole('heading', { level: 1, name: heading })).toBeInTheDocument(); + expect(screen.getByRole('navigation', { name: 'Primary' })).toBeInTheDocument(); + expect(screen.getByRole('contentinfo')).toBeInTheDocument(); + expect(screen.getByRole('main')).toBeInTheDocument(); + }); + + it('sends every Get started and Sign in to sign-up or sign-in (PX-13)', () => { + render(); + const ctas = screen.getAllByRole('link', { name: /^(Get started|Sign in)$/ }); + expect(ctas.length).toBeGreaterThanOrEqual(2); + for (const a of ctas) expect(['/signup', '/signin']).toContain(a.getAttribute('href')); + }); + + it('marks its own link in the navigation', () => { + render(); + const current = within(screen.getByRole('navigation', { name: 'Primary' })).queryAllByRole('link', { current: 'page' }); + expect(current.length).toBeLessThanOrEqual(1); + }); +}); + +describe('PageHero', () => { + it('has the page heading, a supporting sentence and its actions', () => { + render( + + + , + ); + expect(screen.getByRole('heading', { level: 1, name: 'One workspace for every change.' })).toBeInTheDocument(); + expect(screen.getByText('Product')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Get started' })).toBeInTheDocument(); + }); +}); + +describe('FeatureRow', () => { + it('has a heading, bullets and a picture that is hidden from assistive technology', () => { + const { container } = render( + +

card

+
, + ); + expect(screen.getByRole('heading', { level: 2, name: 'Start or connect your site' })).toBeInTheDocument(); + expect(screen.getAllByRole('listitem').map((i) => i.textContent)).toEqual(['No jargon', 'Undo any time']); + expect(container.querySelector('[aria-hidden="true"][inert]')).not.toBeNull(); + }); + + it('can put the picture first', () => { + const { container } = render( + +

card

+
, + ); + expect(container.querySelector('.m-row')).toHaveClass('m-row-reverse'); + }); +}); + +describe('FeatureGrid', () => { + it('shows a heading and one card per feature', () => { + render( + , + ); + expect(screen.getByRole('heading', { level: 2, name: 'Everything you need.' })).toBeInTheDocument(); + expect(screen.getAllByRole('heading', { level: 3 }).map((h) => h.textContent)).toEqual(['Ask in plain words', 'One-click undo']); + }); +}); + +describe('AlwaysNever', () => { + it('lists what Wursor always does and never does', () => { + render(); + expect(screen.getByRole('list', { name: 'Always' })).toBeInTheDocument(); + expect(screen.getByRole('list', { name: 'Never' })).toBeInTheDocument(); + expect(within(screen.getByRole('list', { name: 'Always' })).getAllByRole('listitem')).toHaveLength(5); + expect(within(screen.getByRole('list', { name: 'Never' })).getAllByRole('listitem')).toHaveLength(5); + expect(screen.getByText('Publishes without your approval')).toBeInTheDocument(); + }); +}); + +describe('ComparePlans', () => { + it('compares the three plans in a table', () => { + render(); + const table = screen.getByRole('table', { name: 'Compare plans' }); + expect(within(table).getAllByRole('columnheader').map((c) => c.textContent)).toEqual(['Feature', 'Starter', 'Studio', 'Agency']); + expect(within(table).getAllByRole('row').length).toBeGreaterThan(8); + }); +}); + +describe('PopularArticles', () => { + it('lists articles; only those with a page are links', () => { + render(); + expect(screen.getByRole('heading', { level: 2, name: 'Popular articles' })).toBeInTheDocument(); + expect(screen.getByText('Undo a published change')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'How approval works' })).toHaveAttribute('href', '/trust'); + expect(screen.queryByRole('link', { name: 'Invite your team' })).not.toBeInTheDocument(); + }); +}); + +describe('Pricing page content', () => { + it('does not show prices, allowances or the free trial until they are decided (PX-13)', () => { + render(); + expect(screen.queryByText(/\$\d/)).not.toBeInTheDocument(); + expect(screen.queryByText(/free for 14 days/i)).not.toBeInTheDocument(); + expect(screen.queryByRole('table')).not.toBeInTheDocument(); + expect(screen.getByText('Plans and prices will be published before launch.')).toBeInTheDocument(); + }); +}); + +describe('PricingContent when pricing is confirmed', () => { + it('shows the free trial, the plan cards and the comparison table', () => { + render(); + expect(screen.getByText(/Start free for 14 days/)).toBeInTheDocument(); + expect(screen.getAllByRole('heading', { level: 3, name: /Starter|Studio|Agency/ })).toHaveLength(3); + expect(screen.getByRole('table', { name: 'Compare plans' })).toBeInTheDocument(); + expect(screen.getByText('Draft pricing. Final plans and prices will be confirmed before launch.')).toBeInTheDocument(); + }); +}); + +describe('Docs page content', () => { + it('has a search field, six topics and the popular articles', () => { + render(); + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2, name: 'Browse by topic' })).toBeInTheDocument(); + expect(screen.getAllByRole('heading', { level: 3 })).toHaveLength(6); + expect(screen.getByRole('heading', { level: 2, name: 'Popular articles' })).toBeInTheDocument(); + }); +}); + +describe('How it works page content', () => { + it('walks through the four steps', () => { + render(); + for (const step of ['Start or connect your site', 'Ask for a change', 'Review the proof', 'Publish, or undo']) { + expect(screen.getByRole('heading', { level: 2, name: step })).toBeInTheDocument(); + } + }); +}); + +describe('Trust page content', () => { + it('shows the four rules, the boundaries, the record and the controls', () => { + render(); + expect(screen.getByRole('heading', { level: 3, name: 'Works on a safe copy' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2, name: 'What Wursor does, and what it never does.' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2, name: 'A record you can read.' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2, name: 'You decide who can do what.' })).toBeInTheDocument(); + }); +}); + +describe('Product page content', () => { + it('shows the workspace, six features and where the site lives', () => { + render(); + expect(screen.getByRole('tab', { name: 'Conversation' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 2, name: 'Everything you need to hand off the work.' })).toBeInTheDocument(); + expect(screen.getAllByRole('heading', { level: 3 })).toHaveLength(6); + expect(screen.getByRole('heading', { level: 2, name: 'Choose where it lives. Keep your hands free.' })).toBeInTheDocument(); + }); +}); diff --git a/web/__tests__/product/onboarding.test.tsx b/web/__tests__/product/onboarding.test.tsx new file mode 100644 index 0000000..3f6147a --- /dev/null +++ b/web/__tests__/product/onboarding.test.tsx @@ -0,0 +1,82 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, expect, it, vi } from 'vitest'; +import { ActivityCard } from '../../src/product/ActivityCard.tsx'; +import { Kbd } from '../../src/product/Kbd.tsx'; +import { SearchField } from '../../src/product/SearchField.tsx'; +import { SetupChecklist } from '../../src/product/SetupChecklist.tsx'; + +const STEPS = [ + { state: 'done', title: 'Site address added', detail: 'harborbakery.com' }, + { state: 'done', title: 'Connection verified', detail: 'Secure, read and write' }, + { state: 'next', title: 'Safe copy ready', detail: 'Wursor is creating it' }, + { state: 'todo', title: 'Make your first change', detail: 'Pick a suggestion' }, +] as const; + +describe('SetupChecklist', () => { + it('lists the steps in order, saying which is done, which is next and which is to do', () => { + render(); + const items = within(screen.getByRole('list', { name: 'Setup steps' })).getAllByRole('listitem'); + expect(items).toHaveLength(4); + expect(items.map((i) => i.getAttribute('data-state'))).toEqual(['done', 'done', 'next', 'todo']); + expect(within(items[0]!).getByText('Done')).toBeInTheDocument(); + expect(within(items[2]!).getByText('Next')).toBeInTheDocument(); + expect(within(items[3]!).getByText('To do')).toBeInTheDocument(); + }); + + it('marks the next step as the current one', () => { + render(); + expect(screen.getByText('Safe copy ready').closest('li')).toHaveAttribute('aria-current', 'step'); + }); + + it('offers Continue on the next step only', async () => { + const onContinue = vi.fn(); + render(); + expect(screen.getAllByRole('button', { name: 'Continue' })).toHaveLength(1); + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + expect(onContinue).toHaveBeenCalledOnce(); + }); +}); + +describe('ActivityCard', () => { + const rows = [ + { kind: 'published', text: 'Published the Contact page', time: 'Just now' }, + { kind: 'check', text: 'Checked 9 things, all passed', time: '2 min ago' }, + { kind: 'change', text: 'Changed the Contact page', time: '3 min ago' }, + { kind: 'safe-copy', text: 'Made a safe copy of your site', time: '5 min ago' }, + ] as const; + + it('shows what happened and when, newest first', () => { + render(); + const items = within(screen.getByRole('list', { name: 'Activity' })).getAllByRole('listitem'); + expect(items.map((i) => i.querySelector('.p-activity-text')?.textContent)).toEqual(rows.map((r) => r.text)); + expect(within(items[0]!).getByText('Just now')).toBeInTheDocument(); + }); + + it('gives each kind its own icon', () => { + render(); + const kinds = screen.getAllByRole('listitem').map((i) => i.getAttribute('data-kind')); + expect(kinds).toEqual(['published', 'check', 'change', 'safe-copy']); + }); +}); + +describe('Kbd and SearchField', () => { + it('shows a key cap', () => { + render(⌘K); + expect(screen.getByText('⌘K').tagName).toBe('KBD'); + }); + + it('is a labelled search box with a shortcut hint', async () => { + render(); + const box = screen.getByRole('searchbox', { name: 'Search the docs' }); + expect(box).toHaveAttribute('placeholder', 'Search or ask…'); + expect(screen.getByText('⌘K')).toBeInTheDocument(); + await userEvent.type(box, 'undo'); + expect(box).toHaveValue('undo'); + }); + + it('can be shown but not used until search exists', () => { + render(); + expect(screen.getByRole('searchbox', { name: 'Search the docs' })).toBeDisabled(); + }); +}); diff --git a/web/app/(marketing)/docs/page.tsx b/web/app/(marketing)/docs/page.tsx new file mode 100644 index 0000000..75ba274 --- /dev/null +++ b/web/app/(marketing)/docs/page.tsx @@ -0,0 +1,39 @@ +import type { Metadata } from 'next'; +import { SearchField } from '../../../src/product/SearchField.tsx'; +import { ClosingCta } from '../../../src/marketing/home/ClosingCta.tsx'; +import { FeatureGrid } from '../../../src/marketing/pages/FeatureGrid.tsx'; +import { PageHero } from '../../../src/marketing/pages/PageHero.tsx'; +import { PopularArticles } from '../../../src/marketing/pages/PopularArticles.tsx'; +import { Shell } from '../../../src/marketing/Shell.tsx'; + +export const metadata: Metadata = { title: 'Docs' }; + +export default function DocsPage() { + return ( + + +
+ {/* Search is shown as in the design but not usable until the docs exist. */} + +
+
+ + + +
+ ); +} diff --git a/web/app/(marketing)/how-it-works/page.tsx b/web/app/(marketing)/how-it-works/page.tsx new file mode 100644 index 0000000..ff2843d --- /dev/null +++ b/web/app/(marketing)/how-it-works/page.tsx @@ -0,0 +1,77 @@ +import type { Metadata } from 'next'; +import { ActivityCard } from '../../../src/product/ActivityCard.tsx'; +import { ApprovalCard } from '../../../src/product/ApprovalCard.tsx'; +import { HarborConversation } from '../../../src/product/HarborConversation.tsx'; +import { SetupChecklist } from '../../../src/product/SetupChecklist.tsx'; +import { Zoom } from '../../../src/ui/Zoom.tsx'; +import { Button } from '../../../src/marketing/Button.tsx'; +import { ROUTES } from '../../../src/marketing/content.ts'; +import { ClosingCta } from '../../../src/marketing/home/ClosingCta.tsx'; +import { Faq } from '../../../src/marketing/home/Faq.tsx'; +import { FeatureRow } from '../../../src/marketing/pages/FeatureRow.tsx'; +import { PageHero } from '../../../src/marketing/pages/PageHero.tsx'; +import { ACTIVITY_ROWS, SETUP_STEPS } from '../../../src/marketing/pages/setup-steps.ts'; +import { Shell } from '../../../src/marketing/Shell.tsx'; + +export const metadata: Metadata = { title: 'How it works' }; + +export default function HowItWorksPage() { + return ( + + + + + + + + +
+ +
+ +
+
+
+
+ + + + + + + + +
+ ); +} diff --git a/web/app/(marketing)/page.tsx b/web/app/(marketing)/page.tsx index 0b5c641..29f8f8e 100644 --- a/web/app/(marketing)/page.tsx +++ b/web/app/(marketing)/page.tsx @@ -1,4 +1,4 @@ -import { Footer } from '../../src/marketing/Footer.tsx'; +import { Shell } from '../../src/marketing/Shell.tsx'; import { ApproveWithProof } from '../../src/marketing/home/ApproveWithProof.tsx'; import { ClosingCta } from '../../src/marketing/home/ClosingCta.tsx'; import { Examples } from '../../src/marketing/home/Examples.tsx'; @@ -10,33 +10,28 @@ import { SafeByDesign } from '../../src/marketing/home/SafeByDesign.tsx'; import { SectionHeader } from '../../src/marketing/home/SectionHeader.tsx'; import { SurfacesShowcase } from '../../src/marketing/home/SurfacesShowcase.tsx'; import { WatchTheWork } from '../../src/marketing/home/WatchTheWork.tsx'; -import { Nav } from '../../src/marketing/Nav.tsx'; export default function HomePage() { return ( - <> -