From 46cbf0460bc5729a44bbaaa5f0abe2fd808a98a7 Mon Sep 17 00:00:00 2001 From: Patrick Plate Date: Wed, 24 Jun 2026 14:53:33 +0200 Subject: [PATCH] docs(sprint-1-plan, chunk 3/4): W4 frontend, W5 seed data, W6 deploy + CI/CD --- Sprint-1-Plan-Part-3.md | 474 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 474 insertions(+) create mode 100644 Sprint-1-Plan-Part-3.md diff --git a/Sprint-1-Plan-Part-3.md b/Sprint-1-Plan-Part-3.md new file mode 100644 index 0000000..c3aba14 --- /dev/null +++ b/Sprint-1-Plan-Part-3.md @@ -0,0 +1,474 @@ +# Sprint 1 — Plan, Part 3 ("Spark") + +_Continued from [Sprint-1-Plan-Part-2](Sprint-1-Plan-Part-2.md). Covers W4 (frontend), W5 (seed data), W6 (deploy + CI/CD)._ + +--- + +### W4 — Frontend + +**Goal:** the four humans can see the idea list and post a new idea. PWA manifest is served and "Add to Home Screen" works on iOS Safari + Android Chrome. Service worker registers (stub-only, no caching yet). + +**Pre-requisite:** W3 complete (`/api/ideas` works locally against a logged-in proxy session). + +**Deliverables:** + +1. [`frontend/app/layout.tsx`](../frontend/app/layout.tsx) — root layout, fonts, theme colour, link to manifest. +2. [`frontend/app/(app)/layout.tsx`](../frontend/app/(app)/layout.tsx) — protected layout: redirects to `/login` if no session. +3. [`frontend/app/(app)/page.tsx`](../frontend/app/(app)/page.tsx) — `/` redirects to `/ideas`. +4. [`frontend/app/(app)/ideas/page.tsx`](../frontend/app/(app)/ideas/page.tsx) — list view (server component). +5. [`frontend/app/(app)/ideas/new/page.tsx`](../frontend/app/(app)/ideas/new/page.tsx) — create form (client component). +6. [`frontend/app/(app)/ideas/components/idea-list.tsx`](../frontend/app/(app)/ideas/components/idea-list.tsx). +7. [`frontend/app/(app)/ideas/components/idea-form.tsx`](../frontend/app/(app)/ideas/components/idea-form.tsx). +8. [`frontend/lib/api.ts`](../frontend/lib/api.ts) — thin fetch wrappers. +9. [`frontend/public/manifest.json`](../frontend/public/manifest.json). +10. [`frontend/public/sw.js`](../frontend/public/sw.js) — stub service worker. +11. [`frontend/lib/sw-register.ts`](../frontend/lib/sw-register.ts) — SW registration on first paint. +12. App icon set: `icon-192.png`, `icon-512.png`, `apple-touch-icon.png`, `favicon.ico`. Hand-drawn or `imagemagick`-generated single-colour campfire glyph. See [Open Question Q07](Open-Questions.md#q07-pwa-assets-pipeline). + +**Acceptance gate:** +- Local dev: signed-in user can hit `/ideas`, see an empty list, click "+", post an idea, see it appear. +- `/manifest.json` returns 200 with valid content. +- DevTools → Application → Manifest shows green checkmarks. +- iOS Safari "Add to Home Screen" produces an icon labelled "Sparkboard". +- Android Chrome shows the install prompt. +- Satisfies the UI half of **A4**, all of **A5**, and the post-login navigation half of **A1**. + +**Code sketch — [`frontend/app/(app)/ideas/page.tsx`](../frontend/app/(app)/ideas/page.tsx) (server component):** + +```tsx +import { listIdeas } from "@/lib/api"; +import { IdeaList } from "./components/idea-list"; +import Link from "next/link"; + +export default async function IdeasPage() { + const ideas = await listIdeas(); + + return ( +
+
+

Sparkboard

+ + + New + +
+ +
+ ); +} +``` + +**Code sketch — [`frontend/lib/api.ts`](../frontend/lib/api.ts):** + +```typescript +import { cookies } from "next/headers"; + +export type Idea = { + id: string; + authorId: string; + title: string; + description: string | null; + status: "RAW" | "EXPLORING" | "BUILDING" | "SHIPPED" | "DEAD"; + createdAt: string; + updatedAt: string; +}; + +const ORIGIN = process.env.NEXTAUTH_URL ?? "http://localhost:3000"; + +export async function listIdeas(): Promise { + const res = await fetch(`${ORIGIN}/api/backend/api/ideas`, { + headers: { cookie: (await cookies()).toString() }, + cache: "no-store", + }); + if (!res.ok) throw new Error(`listIdeas: ${res.status}`); + return res.json(); +} + +export async function createIdea(input: { title: string; description?: string }) { + const res = await fetch(`${ORIGIN}/api/backend/api/ideas`, { + method: "POST", + headers: { "content-type": "application/json", cookie: (await cookies()).toString() }, + body: JSON.stringify(input), + }); + if (!res.ok) throw new Error(`createIdea: ${res.status}`); + return res.json(); +} +``` + +**Code sketch — [`frontend/app/(app)/ideas/components/idea-form.tsx`](../frontend/app/(app)/ideas/components/idea-form.tsx) (client component):** + +```tsx +"use client"; +import { useTransition, useState } from "react"; +import { useRouter } from "next/navigation"; + +export function IdeaForm() { + const router = useRouter(); + const [pending, start] = useTransition(); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + start(async () => { + const res = await fetch("/api/backend/api/ideas", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ title, description: description || undefined }), + }); + if (res.ok) router.push("/ideas"); + }); + }; + + return ( +
+ setTitle(e.target.value)} + placeholder="Catch the spark…" + className="w-full text-lg border-b py-2 focus:outline-none" + /> +