+ "body": "Implement a global site header and replace the home page with a 3-card use-case hub. Also closes #125.\n\nDO NOT touch anything under `data/` at any point.\n\n## 1. Transparent logo\n\n`logo.png` exists at the repo root with a white background. Strip it to transparency and save as `public/logo.png`.\n\nInstall sharp: `pnpm add -D sharp`. Then run this one-off script (create, run, delete):\n\n```ts\n// scripts/strip-logo-bg.ts\nimport sharp from 'sharp'\nimport { resolve } from 'path'\nimport { mkdirSync } from 'fs'\n\nmkdirSync('public', { recursive: true })\nconst { data, info } = await sharp(resolve('logo.png')).ensureAlpha().raw().toBuffer({ resolveWithObject: true })\nconst px = new Uint8Array(data)\nfor (let i = 0; i < px.length; i += 4) {\n if (px[i] > 238 && px[i+1] > 238 && px[i+2] > 238) px[i+3] = 0\n}\nawait sharp(Buffer.from(px), { raw: { width: info.width, height: info.height, channels: 4 } }).png().toFile(resolve('public/logo.png'))\n```\n\nRun via SSH: `tsx scripts/strip-logo-bg.ts`. Delete the script after. Commit `public/logo.png`.\n\n## 2. i18n keys\n\nAdd to `messages/en.json`:\n```json\n\"Landing\": {\n \"title\": \"power2plant\",\n \"subtitle\": \"Companion planting garden planner\",\n \"lookupTitle\": \"Look up a crop\",\n \"lookupDesc\": \"Find companions and antagonists for any plant\",\n \"planTitle\": \"Plan beds\",\n \"planDesc\": \"Know what you want to grow — find what fits together\",\n \"gardenTitle\": \"My garden\",\n \"gardenDesc\": \"See how the plants in your beds get along\"\n},\n\"Nav\": {\n \"lookup\": \"Look up a crop\",\n \"plan\": \"Plan beds\",\n \"garden\": \"My garden\"\n}\n```\n\nAdd to `messages/de.json`:\n```json\n\"Landing\": {\n \"title\": \"power2plant\",\n \"subtitle\": \"Begleitpflanzung — Gartenplaner\",\n \"lookupTitle\": \"Pflanze nachschlagen\",\n \"lookupDesc\": \"Begleiter und Antagonisten für jede Pflanze finden\",\n \"planTitle\": \"Beete planen\",\n \"planDesc\": \"Du weißt, was du anbauen willst — finde, was zusammenpasst\",\n \"gardenTitle\": \"Mein Garten\",\n \"gardenDesc\": \"Schau, wie die Pflanzen in deinen Beeten miteinander auskommen\"\n},\n\"Nav\": {\n \"lookup\": \"Pflanze nachschlagen\",\n \"plan\": \"Beete planen\",\n \"garden\": \"Mein Garten\"\n}\n```\n\n## 3. SiteHeader component\n\nCreate `src/components/site-header.tsx`:\n\n```tsx\n'use client'\nimport Image from 'next/image'\nimport { Link, usePathname } from '@/i18n/navigation'\nimport { useTranslations } from 'next-intl'\nimport { LocaleSwitcher } from '@/components/locale-switcher'\nimport { AuthPanel } from '@/components/auth-panel'\n\nconst NAV_ITEMS = [\n { key: 'lookup' as const, href: '/relationships' },\n { key: 'plan' as const, href: '/plan' },\n { key: 'garden' as const, href: '/garden' },\n] satisfies { key: 'lookup' | 'plan' | 'garden'; href: string }[]\n\nfunction isActive(pathname: string, href: string): boolean {\n if (href === '/relationships') {\n return pathname.startsWith('/relationships') || pathname.startsWith('/plants')\n }\n return pathname === href || pathname.startsWith(href + '/')\n}\n\nexport function SiteHeader() {\n const t = useTranslations('Nav')\n const pathname = usePathname()\n\n return (\n <header className=\"border-b bg-background sticky top-0 z-40\">\n <div className=\"max-w-5xl mx-auto px-4 h-14 flex items-center gap-6\">\n <Link href=\"/\" className=\"flex items-center gap-2 shrink-0\">\n <Image src=\"/logo.png\" alt=\"\" width={32} height={32} />\n <span className=\"font-semibold text-sm\">power2plant</span>\n </Link>\n <nav className=\"flex items-center gap-1 flex-1 flex-wrap\">\n {NAV_ITEMS.map(({ key, href }) => (\n <Link\n key={key}\n href={href}\n className={`px-3 py-1.5 rounded text-sm transition-colors ${\n isActive(pathname, href)\n ? 'font-semibold text-foreground underline decoration-primary decoration-2 underline-offset-4'\n : 'text-muted-foreground hover:text-foreground'\n }`}\n >\n {t(key)}\n </Link>\n ))}\n </nav>\n <div className=\"flex items-center gap-3 shrink-0\">\n <LocaleSwitcher />\n <AuthPanel />\n </div>\n </div>\n </header>\n )\n}\n```\n\n## 4. Route groups\n\nNext.js route groups (folders in parentheses) don't add URL segments. Use them to apply SiteHeader only to app pages, not the landing page.\n\nCreate `src/app/[locale]/(app)/layout.tsx`:\n```tsx\nimport { SiteHeader } from '@/components/site-header'\nexport default function AppLayout({ children }: { children: React.ReactNode }) {\n return (<><SiteHeader />{children}</>)\n}\n```\n\nMove these directories into `(app)/` using `git mv`:\n- `src/app/[locale]/relationships/` → `src/app/[locale]/(app)/relationships/`\n- `src/app/[locale]/garden/` → `src/app/[locale]/(app)/garden/`\n- `src/app/[locale]/contribute/` → `src/app/[locale]/(app)/contribute/`\n- `src/app/[locale]/plants/` → `src/app/[locale]/(app)/plants/`\n- `src/app/[locale]/share/` → `src/app/[locale]/(app)/share/`\n\nAfter moving, strip the inline per-page headers from each moved page — they are now handled by SiteHeader:\n- `(app)/garden/page.tsx`: remove the header div containing `LocaleSwitcher`, `AuthPanel`, `backHome` link, and h1+subtitle. Replace with just `<h1 className=\"text-3xl font-bold\">{t('title')}</h1>` and `<p className=\"text-muted-foreground mt-1\">{t('subtitle')}</p>` in a plain `<div>`. Remove `Link`, `AuthPanel`, `LocaleSwitcher` imports.\n- `(app)/relationships/page.tsx`: remove the `backHome` Link element. Remove `Link` import from `@/i18n/navigation` if it becomes unused (check if Link is used elsewhere in the file first).\n- `(app)/contribute/page.tsx`: remove the `back` Link element (the `<Link href=\"/\">` near the top of the return). Remove `Link` import if unused.\n- `(app)/share/[token]/page.tsx`: remove the `backHome` Link element. Remove `Link` import if unused.\n- `(app)/plants/[id]/page.tsx`: check for any backHome/LocaleSwitcher/AuthPanel and remove if present.\n\n## 5. /plan page and landing hub (atomic commit)\n\nCreate `src/app/[locale]/(app)/plan/page.tsx` — copy from the current `src/app/[locale]/page.tsx` with these changes:\n- Remove the entire top `<div className=\"flex items-start justify-between gap-4\">` block (the block containing the h1 title, subtitle, contributeLink, browseLink, LocaleSwitcher, and AuthPanel)\n- Remove the `<Separator />` component and its import\n- Remove imports: `LocaleSwitcher`, `AuthPanel`, `Separator` (keep `Link` for the MyGarden button)\n- Keep all planner state and logic unchanged\n- Keep the centered MyGarden button (`<Link href=\"/garden\">...`)\n\nCreate `src/app/[locale]/(landing)/page.tsx`:\n```tsx\n'use client'\nimport Image from 'next/image'\nimport { useTranslations } from 'next-intl'\nimport { Link } from '@/i18n/navigation'\nimport { LocaleSwitcher } from '@/components/locale-switcher'\nimport { AuthPanel } from '@/components/auth-panel'\n\nconst CARDS = [\n { titleKey: 'lookupTitle', descKey: 'lookupDesc', href: '/relationships' },\n { titleKey: 'planTitle', descKey: 'planDesc', href: '/plan' },\n { titleKey: 'gardenTitle', descKey: 'gardenDesc', href: '/garden' },\n] as const\n\nexport default function LandingPage() {\n const t = useTranslations('Landing')\n return (\n <main className=\"min-h-screen px-4 py-8\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"flex justify-end items-center gap-3 mb-8\">\n <LocaleSwitcher />\n <AuthPanel />\n </div>\n <div className=\"flex flex-col items-center text-center mb-12\">\n <Image src=\"/logo.png\" alt=\"\" width={64} height={64} className=\"mb-4\" />\n <h1 className=\"text-3xl font-bold\">{t('title')}</h1>\n <p className=\"text-muted-foreground mt-2\">{t('subtitle')}</p>\n </div>\n <div className=\"grid grid-cols-1 sm:grid-cols-3 gap-4\">\n {CARDS.map(({ titleKey, descKey, href }) => (\n <Link key={href} href={href} className=\"flex flex-col gap-2 rounded-xl border bg-card p-6 hover:shadow-md transition-shadow\">\n <h2 className=\"font-semibold\">{t(titleKey)}</h2>\n <p className=\"text-sm text-muted-foreground\">{t(descKey)}</p>\n </Link>\n ))}\n </div>\n </div>\n </main>\n )\n}\n```\n\nDelete `src/app/[locale]/page.tsx` with `git rm`. These three changes (plan page, landing page, delete old page) must be in one commit.\n\n## 6. Update e2e smoke tests\n\nIn `tests/e2e/smoke.test.ts`, replace these two existing tests:\n- `'home page loads with app heading'`\n- `'home page has contribute nav link'`\n\nWith:\n```ts\ntest('landing page shows 3 use-case cards', async ({ page }) => {\n await page.goto('/')\n await expect(page.getByRole('heading', { name: 'power2plant' })).toBeVisible()\n await expect(page.getByRole('link', { name: /look up a crop/i })).toBeVisible()\n await expect(page.getByRole('link', { name: /plan beds/i })).toBeVisible()\n await expect(page.getByRole('link', { name: /my garden/i })).toBeVisible()\n})\n\ntest('plan page has site header with nav', async ({ page }) => {\n await page.goto('/plan')\n await expect(page.getByRole('banner')).toBeVisible()\n await expect(page.getByRole('link', { name: /look up a crop/i })).toBeVisible()\n})\n```\n\n## 7. Verification\n\nRun via SSH before each commit:\n- `pnpm exec tsc --noEmit` — must pass with no errors\n- `pnpm test:run` — all unit tests must pass\n- Final: `pnpm build` — must complete without errors"
0 commit comments