# Template Feature Completion — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Complete the five simulated terminal-write flows in the starter templates so they persist via Supabase when a backend is configured, with graceful demo fallback otherwise.

**Architecture:** Each affected template gets a tiny `src/lib/submit.ts` (`submitOrSimulate`) wrapping the existing `useSupabaseTable` write path. The five handlers (ecommerce Checkout, dashboard Profile, cms PostEditor, landing/portfolio Contact) route their submit through it; two read-backs (ecommerce Account, cms Admin) switch hardcoded sample arrays to the live tables via `useData`. Tables are agent-created via `defineTable`, documented in each `KNOWLEDGE.md`. Changes are applied to extracted zips and re-zipped via the WS1 binary-safe path.

**Tech Stack:** React/TypeScript/Vite templates; `useSupabaseTable`/`useData` hooks; Python `zipfile` for re-packaging.

Reference spec: `specs/2026-06-03-template-feature-completion-design.md`

---

## File Structure

Working copies are extracted to `/tmp/ws2build/<template>/`. Per affected template (`ecommerce`, `cms`, `dashboard`, `landing`, `portfolio`):

- **Create** `src/lib/submit.ts` — the `submitOrSimulate` helper (identical in all five).
- **Modify** the handler page(s) + read-back page(s) listed per task.
- **Modify** `KNOWLEDGE.md` — add a "Starter persistence" section.

`default` template is untouched. Re-zip overwrites `storage/app/private/templates/<t>-template.zip`.

---

## Task 1: Extract working copies

**Files:** none (scratch extraction).

- [ ] **Step 1: Confirm clean git state for the template zips**

Run: `cd /Users/noriellecruz/Web/webby && git status --short storage/app/private/templates/`
Expected: no output.

- [ ] **Step 2: Extract the five affected zips**

Run:
```bash
cd /tmp && rm -rf ws2build && mkdir ws2build && cd ws2build
SRC=/Users/noriellecruz/Web/webby/storage/app/private/templates
for t in ecommerce cms dashboard landing portfolio; do unzip -oq "$SRC/$t-template.zip" -d "$t"; done
ls */src/lib/submit.ts 2>/dev/null || echo "no submit.ts yet (expected)"
```
Expected: prints `no submit.ts yet (expected)`.

---

## Task 2: Add the `submitOrSimulate` helper to all five templates

**Files:**
- Create: `/tmp/ws2build/{ecommerce,cms,dashboard,landing,portfolio}/src/lib/submit.ts`

- [ ] **Step 1: Write the helper into each template**

Create this exact file at `src/lib/submit.ts` in all five extracted templates:

```ts
import { isSupabaseConfigured } from './supabase'

export interface SubmitResult {
  /** True when the record was persisted, or simulated in demo mode. */
  ok: boolean
  /** True when no backend is configured and the write was simulated. */
  demo: boolean
  /** New row id when a real insert succeeded. */
  id?: string
}

/**
 * Run a real write when a Supabase backend is configured; otherwise simulate
 * (demo mode) so the starter flow still completes. Never throws.
 *
 * `write` performs the actual persistence via the established data layer
 * (e.g. `() => table.add(row)` from useSupabaseTable), returning the new id (or
 * any non-null value) on success and null on failure.
 */
export async function submitOrSimulate(
  write: () => Promise<string | null>,
  opts: { simulateMs?: number } = {},
): Promise<SubmitResult> {
  if (!isSupabaseConfigured()) {
    await new Promise((r) => setTimeout(r, opts.simulateMs ?? 600))
    return { ok: true, demo: true }
  }
  const id = await write()
  return id ? { ok: true, demo: false, id } : { ok: false, demo: false }
}
```

- [ ] **Step 2: Verify the helper imports a real export**

Run: `grep -n "isSupabaseConfigured" /tmp/ws2build/ecommerce/src/lib/supabase.ts`
Expected: a matching `export function isSupabaseConfigured` line (confirms the import target exists).

---

## Task 3: Wire the Contact forms (landing + portfolio)

Both files are byte-identical. Apply the same edit to each.

**Files:**
- Modify: `/tmp/ws2build/landing/src/pages/Contact.tsx`
- Modify: `/tmp/ws2build/portfolio/src/pages/Contact.tsx`

- [ ] **Step 1: Add the imports**

In each file, after `import { usePageMeta } from '../hooks/usePageMeta';` add:
```ts
import { useSupabaseTable } from '../hooks/useSupabaseTable';
import { submitOrSimulate } from '../lib/submit';
```

- [ ] **Step 2: Add the write hook and rewrite the handler**

In each file, replace this exact block:
```ts
  const [form, setForm] = useState({ name: '', email: '', message: '' });
  const [submitting, setSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitting(true);
    // Simulate sending the message.
    await new Promise((resolve) => setTimeout(resolve, 800));
    setSubmitting(false);
    setForm({ name: '', email: '', message: '' });
    toast.success("Thanks for reaching out! We'll get back to you soon.");
  };
```
with:
```ts
  const [form, setForm] = useState({ name: '', email: '', message: '' });
  const [submitting, setSubmitting] = useState(false);
  const messages = useSupabaseTable('contact_messages', { enabled: false });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitting(true);
    const res = await submitOrSimulate(() => messages.add(form), { simulateMs: 800 });
    setSubmitting(false);
    if (!res.ok) {
      toast.error('Could not send your message. Please try again.');
      return;
    }
    setForm({ name: '', email: '', message: '' });
    toast.success("Thanks for reaching out! We'll get back to you soon.");
  };
```

- [ ] **Step 3: Verify no un-gated simulated send remains**

Run: `grep -nE "setTimeout|submitOrSimulate" /tmp/ws2build/landing/src/pages/Contact.tsx /tmp/ws2build/portfolio/src/pages/Contact.tsx`
Expected: each file shows `submitOrSimulate` and no bare `setTimeout` in the handler.

---

## Task 4: Wire ecommerce Checkout to persist an order

**Files:**
- Modify: `/tmp/ws2build/ecommerce/src/pages/Checkout.tsx`

- [ ] **Step 1: Add imports**

After `import { useCart } from '../hooks/useCart';` add:
```ts
import { useSupabaseTable } from '../hooks/useSupabaseTable';
import { submitOrSimulate } from '../lib/submit';
```

- [ ] **Step 2: Add the orders hook**

Immediately after `const { items: orderItems, cartTotal, clearCart } = useCart();` add:
```ts
  const orders = useSupabaseTable('orders', { enabled: false });
```

- [ ] **Step 3: Rewrite `handlePaymentSubmit`**

Replace this exact block:
```ts
  const handlePaymentSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);

    // Simulate payment processing
    await new Promise(resolve => setTimeout(resolve, 2000));

    setLoading(false);
    setStep(3);
    clearCart();
    toast.success('Order placed successfully!');
  };
```
with:
```ts
  const handlePaymentSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);

    const res = await submitOrSimulate(
      () => orders.add({
        items: orderItems.map((i) => ({ name: i.name, quantity: i.quantity, price: i.price })),
        subtotal,
        shipping,
        tax,
        total,
        shipping_address: shippingInfo,
        status: 'processing',
      }),
      { simulateMs: 1500 },
    );

    setLoading(false);
    if (!res.ok) {
      toast.error('Could not place your order. Please try again.');
      return;
    }
    setStep(3);
    clearCart();
    toast.success('Order placed successfully!');
  };
```

- [ ] **Step 4: Verify**

Run: `grep -nE "setTimeout|submitOrSimulate|orders.add" /tmp/ws2build/ecommerce/src/pages/Checkout.tsx`
Expected: `submitOrSimulate` + `orders.add` present, no bare `setTimeout` in the handler.

---

## Task 5: Read-back — ecommerce Account lists real orders

The page currently uses a module-level `const orders = [ ...sample... ]`. Switch to the live `orders` table with the sample as fallback, and normalize the date field (real rows have `created_at`; sample rows have `date`).

**Files:**
- Modify: `/tmp/ws2build/ecommerce/src/pages/Account.tsx`

- [ ] **Step 1: Rename the sample constant**

Change `// Sample orders data` + `const orders = [` to `// Sample orders (shown in demo mode when no backend is configured)` + `const SAMPLE_ORDERS = [`. Leave the array contents unchanged.

- [ ] **Step 2: Add imports (if not already present)**

Ensure these imports exist near the top of the file:
```ts
import { useData } from '../hooks/useData';
import { formatDate } from '../lib/format';
```

- [ ] **Step 3: Read orders from the table inside the component**

Immediately after `const { user, signOut } = useAuth();` add:
```ts
  const { data: orders } = useData<{
    id: string; date?: string; created_at?: string; status: string; total: number;
    items: { name: string; quantity: number; price: number }[];
  }>('orders', SAMPLE_ORDERS, { orderBy: 'created_at', ascending: false });
```

- [ ] **Step 4: Normalize the date in the order render**

In the orders `.map((order) => ...)`, replace the date display `{order.date}` with:
```tsx
{order.date ?? (order.created_at ? formatDate(order.created_at) : '')}
```

- [ ] **Step 5: Verify**

Run: `grep -nE "SAMPLE_ORDERS|useData<|formatDate" /tmp/ws2build/ecommerce/src/pages/Account.tsx`
Expected: `SAMPLE_ORDERS` defined + passed to `useData`, `formatDate` used. Confirm `formatDate` is exported: `grep -n "export.*formatDate" /tmp/ws2build/ecommerce/src/lib/format.ts`.

---

## Task 6: Wire dashboard Profile to persist (add-or-update)

Profile is `ProtectedRoute`-gated, so a user is present. Use `useSupabaseTable('profiles')` (fetch on) to know whether the user's row exists, then add or update. No form pre-fill (YAGNI).

**Files:**
- Modify: `/tmp/ws2build/dashboard/src/pages/Profile.tsx`

- [ ] **Step 1: Add imports**

After `import { toast } from 'sonner';` add:
```ts
import { useSupabaseTable } from '../hooks/useSupabaseTable';
import { submitOrSimulate } from '../lib/submit';
```

- [ ] **Step 2: Add the profiles hook**

Immediately after `const [saving, setSaving] = useState(false);` add:
```ts
  const profiles = useSupabaseTable<{ id: string }>('profiles');
```

- [ ] **Step 3: Rewrite `handleSave`**

Replace this exact block:
```ts
  const handleSave = async () => {
    setSaving(true);
    await new Promise(resolve => setTimeout(resolve, 1000));
    setSaving(false);
    toast.success('Profile updated successfully');
  };
```
with:
```ts
  const handleSave = async () => {
    setSaving(true);
    const fields = {
      first_name: profile.firstName,
      last_name: profile.lastName,
      phone: profile.phone,
      location: profile.location,
      company: profile.company,
      role: profile.role,
      bio: profile.bio,
      website: profile.website,
    };
    const existing = profiles.data[0];
    const res = await submitOrSimulate(
      () => existing
        ? profiles.update(existing.id, fields).then((ok) => (ok ? existing.id : null))
        : profiles.add(fields),
      { simulateMs: 1000 },
    );
    setSaving(false);
    if (!res.ok) {
      toast.error('Could not save your profile. Please try again.');
      return;
    }
    toast.success('Profile updated successfully');
  };
```

- [ ] **Step 4: Verify**

Run: `grep -nE "setTimeout|submitOrSimulate|profiles\.(add|update)" /tmp/ws2build/dashboard/src/pages/Profile.tsx`
Expected: `submitOrSimulate` + `profiles.update`/`profiles.add`, no bare `setTimeout` in the handler.

---

## Task 7: Wire cms PostEditor to persist (create or edit)

`isEditing` is `Boolean(id)`. Create → `add`; edit → `update(id, ...)`. Writes to the same `posts` table the Admin list reads (Task 8 wires that read).

**Files:**
- Modify: `/tmp/ws2build/cms/src/pages/PostEditor.tsx`

- [ ] **Step 1: Add imports**

After `import { useSystemStorage } from '../hooks/useSystemStorage';` add:
```ts
import { useSupabaseTable } from '../hooks/useSupabaseTable';
import { submitOrSimulate } from '../lib/submit';
```

- [ ] **Step 2: Add the posts hook**

Immediately after `const { upload, uploading, error: uploadError, isConfigured } = useSystemStorage();` add:
```ts
  const postsTable = useSupabaseTable('posts', { enabled: false });
```

- [ ] **Step 3: Rewrite the persist portion of `handleSave`**

Replace this exact block:
```ts
    setSaving(true);
    await new Promise(resolve => setTimeout(resolve, 1000));

    setPost({ ...post, status });
    toast.success(status === 'published' ? 'Post published!' : 'Draft saved!');
    setSaving(false);

    if (status === 'published') {
      navigate('/admin');
    }
```
with:
```ts
    setSaving(true);
    const row = { ...post, status };
    const res = await submitOrSimulate(
      () => isEditing
        ? postsTable.update(id as string, row).then((ok) => (ok ? (id as string) : null))
        : postsTable.add(row),
      { simulateMs: 1000 },
    );
    setSaving(false);
    if (!res.ok) {
      toast.error('Could not save your post. Please try again.');
      return;
    }
    setPost(row);
    toast.success(status === 'published' ? 'Post published!' : 'Draft saved!');

    if (status === 'published') {
      navigate('/admin');
    }
```

- [ ] **Step 4: Verify**

Run: `grep -nE "setTimeout|submitOrSimulate|postsTable\.(add|update)" /tmp/ws2build/cms/src/pages/PostEditor.tsx`
Expected: `submitOrSimulate` + `postsTable.add`/`postsTable.update`, no bare `setTimeout` in the handler.

---

## Task 8: Read-back — cms Admin lists real posts

Admin uses `const [posts, setPosts] = useState(recentPosts)` (sample) and deletes via `setPosts(posts.filter(...))`. Switch to the live `posts` table via `useData` with the sample as fallback; delete via `remove`.

**Files:**
- Modify: `/tmp/ws2build/cms/src/pages/Admin.tsx`

- [ ] **Step 1: Rename the sample constant**

Change `// Sample recent posts for admin` + `const recentPosts = [` to `// Sample posts (shown in demo mode when no backend is configured)` + `const SAMPLE_POSTS = [`. Leave the array contents unchanged.

- [ ] **Step 2: Add the import**

Near the top, add:
```ts
import { useData } from '../hooks/useData';
```

- [ ] **Step 3: Replace the local state with the data hook**

Replace `const [posts, setPosts] = useState(recentPosts);` with:
```ts
  const { data: posts, remove } = useData('posts', SAMPLE_POSTS, { orderBy: 'created_at', ascending: false });
```

- [ ] **Step 4: Rewrite the delete handler**

Replace the body that does `setPosts(posts.filter(p => p.id !== id))` with a call to `remove(id)`. The handler becomes:
```ts
  const handleDelete = async (id: string | number) => {
    await remove(String(id));
    toast.success('Post deleted');
  };
```
(If the existing handler has a different name, keep the name; only change the body to `await remove(String(id)); toast.success('Post deleted');`.)

- [ ] **Step 5: Drop the now-unused `useState` import if nothing else uses it**

Run: `grep -nE "useState" /tmp/ws2build/cms/src/pages/Admin.tsx`
If `useState` has no other usage, remove it from the `import { useState } from 'react'` line. If other usages exist, leave it.

- [ ] **Step 6: Verify**

Run: `grep -nE "SAMPLE_POSTS|useData|remove\(" /tmp/ws2build/cms/src/pages/Admin.tsx`
Expected: `SAMPLE_POSTS` passed to `useData`, `remove(` used in delete.

---

## Task 9: Add "Starter persistence" guidance to each KNOWLEDGE.md

**Files:**
- Modify: `KNOWLEDGE.md` in `/tmp/ws2build/{ecommerce,cms,dashboard,landing,portfolio}`

- [ ] **Step 1: Append the section to each**

Insert a "## Starter persistence" section immediately before the "## Theme & styling" section (so it sits with the feature docs) in each template, with the template-specific table spec:

**ecommerce:**
```markdown
## Starter persistence
Checkout writes an order to the `orders` table via `useSupabaseTable`; Account
lists the user's orders from it. When a Supabase backend is enabled, create the
table with the `defineTable` tool (id/created_at/user_id + RLS are added
automatically); without a backend these flows run in demo mode (simulated).
- `orders`: items (jsonb), subtotal (numeric), shipping (numeric), tax (numeric), total (numeric), shipping_address (jsonb), status (text)
```

**cms:**
```markdown
## Starter persistence
PostEditor creates/updates rows in the `posts` table via `useSupabaseTable`;
the Admin list reads from it. When a Supabase backend is enabled, create the
table with the `defineTable` tool (id/created_at/user_id + RLS are added
automatically); without a backend these flows run in demo mode (simulated).
- `posts`: title (text), excerpt (text), content (text), category (text), image (text), status (text)
```

**dashboard:**
```markdown
## Starter persistence
Profile saves to the `profiles` table via `useSupabaseTable` (one row per user).
When a Supabase backend is enabled, create the table with the `defineTable` tool
(id/created_at/user_id + RLS are added automatically); without a backend the
save runs in demo mode (simulated).
- `profiles`: first_name (text), last_name (text), phone (text), location (text), company (text), role (text), bio (text), website (text)
```

**landing** and **portfolio** (identical):
```markdown
## Starter persistence
The Contact form writes to the `contact_messages` table via `useSupabaseTable`.
When a Supabase backend is enabled, create the table with the `defineTable` tool
(id/created_at/user_id + RLS are added automatically); without a backend the
form runs in demo mode (simulated).
- `contact_messages`: name (text), email (text), message (text)
```

- [ ] **Step 2: Verify**

Run: `for t in ecommerce cms dashboard landing portfolio; do echo "$t: $(grep -c 'Starter persistence' /tmp/ws2build/$t/KNOWLEDGE.md)"; done`
Expected: each `1`.

---

## Task 10: Build each modified template standalone

Validates every edit compiles and types check.

**Files:** read-only build check.

- [ ] **Step 1: Build all five**

Run:
```bash
for t in ecommerce cms dashboard landing portfolio; do
  echo "=== $t ==="
  (cd /tmp/ws2build/$t && npm install --no-audit --no-fund --silent && npm run build 2>&1 | tail -3)
done
```
Expected: each prints a successful `vite build` (`✓ built`), no TypeScript errors. (If a build fails on a type mismatch, fix the offending handler edit — most likely a hook generic or an import path — and rebuild that template.)

---

## Task 11: Re-bake the five zips (binary-safe) and verify

**Files:**
- Modify: `storage/app/private/templates/{ecommerce,cms,dashboard,landing,portfolio}-template.zip`

- [ ] **Step 1: Re-zip each working copy in place**

Re-pack only the source files that belong in the zip (exclude `node_modules`, `dist`, build caches). Run:
```bash
SRC=/Users/noriellecruz/Web/webby/storage/app/private/templates
for t in ecommerce cms dashboard landing portfolio; do
  ( cd /tmp/ws2build/$t && rm -rf node_modules dist .vite && rm -f "$SRC/$t-template.zip" && zip -rq "$SRC/$t-template.zip" . -x "*.DS_Store" )
  echo "$t re-zipped"
done
```
Expected: five `<t> re-zipped` lines.

- [ ] **Step 2: Verify each zip contains the new files and no residue**

Run:
```bash
cd /tmp && rm -rf ws2verify && mkdir ws2verify && cd ws2verify
SRC=/Users/noriellecruz/Web/webby/storage/app/private/templates
for t in ecommerce cms dashboard landing portfolio; do
  unzip -oq "$SRC/$t-template.zip" -d "$t"
  helper=$(test -f "$t/src/lib/submit.ts" && echo yes || echo NO)
  know=$(grep -c "Starter persistence" "$t/KNOWLEDGE.md")
  sim=$(grep -rlE "Simulate (payment|sending)" "$t/src" 2>/dev/null | grep -v node_modules | wc -l | tr -d ' ')
  echo "$t: helper=$helper knowledge=$know stale_sim_comments=$sim"
done
```
Expected: each `helper=yes knowledge=1 stale_sim_comments=0`.

- [ ] **Step 3: Confirm binary-safe storage (no git round-trip drift)**

Run:
```bash
cd /Users/noriellecruz/Web/webby
git add storage/app/private/templates/*-template.zip
git status --short storage/app/private/templates/
```
Expected: five `M ...-template.zip` lines (and after commit in Task 12, `git status` will be clean — `.gitattributes` already marks `*.zip binary`).

---

## Task 12: Commit and clean up

**Files:**
- Commit: the five modified zips.
- Delete: `/tmp/ws2build`, `/tmp/ws2verify`.

- [ ] **Step 1: Commit**

Run:
```bash
cd /Users/noriellecruz/Web/webby
git commit -m "$(cat <<'EOF'
feat(templates): complete starter write flows with real persistence

Replace the simulated terminal writes (ecommerce checkout, cms post publish,
dashboard profile, landing/portfolio contact) with real Supabase persistence
via a submitOrSimulate helper around useSupabaseTable, keeping demo-mode
simulation when no backend is configured and surfacing honest errors otherwise.
Read-backs: ecommerce Account and cms Admin now read their live tables (orders,
posts) with the sample arrays as demo fallback. Each KNOWLEDGE.md documents the
starter table for the agent's defineTable tool.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)"
git log --oneline -1
```
Expected: commit created.

- [ ] **Step 2: Verify HEAD zips are valid + clean**

Run:
```bash
cd /Users/noriellecruz/Web/webby
git status --short storage/app/private/templates/ || true
git show HEAD:storage/app/private/templates/ecommerce-template.zip > /tmp/c.zip
unzip -t /tmp/c.zip >/dev/null 2>&1 && echo "ecommerce HEAD zip: VALID" || echo "CORRUPT"
unzip -p /tmp/c.zip src/lib/submit.ts | grep -c submitOrSimulate
rm -f /tmp/c.zip
```
Expected: no status output (clean), `ecommerce HEAD zip: VALID`, and `1`.

- [ ] **Step 3: Clean up**

Run: `rm -rf /tmp/ws2build /tmp/ws2verify && echo cleaned`
Expected: `cleaned`.

---

## Self-Review (completed by plan author)

- **Spec coverage:** helper (Task 2), 5 handler rewrites (Tasks 3,4,6,7), read-backs (Tasks 5,8), KNOWLEDGE.md guidance (Task 9), fallback/error semantics (in every handler via `submitOrSimulate` + `if (!res.ok) toast.error`), standalone build test (Task 10), residue/helper/knowledge checks (Task 11), binary-safe packaging + commit (Tasks 11-12). Backend-enabled persistence verification is environment-dependent (needs a Supabase-enabled build) and is covered by the existing running stack but not scripted here — noted as optional manual follow-up.
- **Placeholder scan:** none — full helper code, full before/after for each handler, exact table specs, exact commands. The KNOWLEDGE.md `<flow>` wording is replaced by concrete per-template text in Task 9.
- **Type/name consistency:** `submitOrSimulate`/`SubmitResult` used identically everywhere; write thunks return `Promise<string|null>` matching `useSupabaseTable.add`/`.update().then(...)`; table names (`contact_messages`, `orders`, `profiles`, `posts`) consistent between handler writes, read-backs, and KNOWLEDGE.md specs; `useData(table, sample, opts)` and `useSupabaseTable(table, {enabled})` signatures match the hooks read during exploration.
