Skip to content

fix(web): broken duel link + create-duel CTA on /duel landing#59

Merged
Calixteair merged 2 commits into
mainfrom
feat/duel-share-and-fix
May 13, 2026
Merged

fix(web): broken duel link + create-duel CTA on /duel landing#59
Calixteair merged 2 commits into
mainfrom
feat/duel-share-and-fix

Conversation

@Calixteair
Copy link
Copy Markdown
Owner

What

Two related changes around the duel flow:

  1. Bug fix — opening a shared duel link, clicking "Play this grid", left the player on an empty section: cells never appeared.
  2. Feature — visiting `/duel` without query params now lets you create a fresh shareable grid in one click (was a dead-end error page before).

Why

The bug

Friend clicks `/duel?id=…&sig=…` → `DuelView` renders the summary + "Play this grid" CTA → click flips `screen = "playing"` → mounts ``. But `Grid.loadGrid` had a guard:

```ts
if (mode === "duel") {
gridLoading = false;
return; // ← never calls startGame
}
```

With a "consent step matters" comment that was already wrong: the consent step happens upstream on `/duel`. By the time Grid mounts, the player has already clicked the CTA. Same shape of bug we hit on `/play` in #56.

`store.state` stays null → none of the `{#if}/{:else if}` branches in the template match → blank section.

The dead-end

`/duel` without query params hit `m.duel_missing_link()` then showed a "back to home" link. With duel mode now stable, `/duel` is a natural entry point for "I want to share a grid" — make it work that way.

How

Bug fix (`Grid.svelte::loadGrid`)

Mirror solo's mount logic, pinned to `duelGridId`:

```ts
if (mode === "duel") {
if (duelGridId && store.state && store.state.gridId !== duelGridId) {
store.clear();
}
const hasActiveDuel =
store.state !== null && !store.state.ended &&
store.grid !== null && store.state.gridId === duelGridId;
if (!hasActiveDuel) {
await startGame();
}
gridLoading = false;
return;
}
```

  • Clear stale session pointing at another grid.
  • Resume an in-flight session for this exact grid.
  • Otherwise POST `/api/games {mode:"duel", duelGridId}` so the server pins the grid and the store seeds.

Create CTA (`DuelView.svelte`)

Refactor the component around a typed `screen` state machine:

  • `landing`: no query params → "Create a grid to share" CTA. Click POSTs `/api/duels`, fires the Web Share API, falls back to clipboard. A "Share again / Copy" panel persists once the link exists.
  • `view`: `?id=&sig=` resolved → players list + "Play this grid" (unchanged).
  • `playing`: mounts `Grid` (now fixed).

Broken/expired duel links (401/404/410) drop back to `landing` with the error banner above the CTA, so a friend who got a stale link can immediately spin up a fresh one instead of being stuck.

Checklist

  • Conventional commit titles
  • No mention of AI / Claude / Anthropic / Copilot
  • `pnpm --dir web lint && typecheck && test` passes (25 tests)
  • `pnpm --dir web build` succeeds (9 pages)
  • Mobile-first respected
  • No new `unsafe`, no `unwrap()` in non-test code
  • No new secret
  • No contract change (web-only fixes against existing OpenAPI)

Test plan

  • Open a shared duel link → click "Play this grid" → cells appear immediately
  • Open the link a second time on the same browser → resume the in-flight game
  • Open the link, refresh mid-game → state restored
  • Open a stale link (401/410) → fallback to landing with error banner + Create CTA still works
  • Visit `/duel` directly → landing screen with Create CTA
  • Click Create → share sheet fires on mobile, link displayed + copied on desktop
  • Open the freshly created link in a private window → view screen, then play

When a friend clicks the shared duel link, /duel loads, displays the
players summary, and they click 'Play this grid'. That mounted
<Grid mode="duel" duelGridId={…} />, but Grid.loadGrid short-circuited
on duel mode without ever calling startGame — store.state stayed null,
no template branch matched, the section rendered empty. Exactly the
same bug we hit on /play in #56, just on a different mode.

The 'consent step matters' comment in the previous version was wrong:
the consent already happened upstream on /duel via the explicit CTA.
By the time Grid mounts with mode=duel + duelGridId, we should
immediately POST /games to materialise the cells.

Mirror solo's mount logic but pinned to duelGridId: clear the store if
it holds a different gridId, resume if it holds an unfinished session
for this duel, otherwise startGame().
/duel without ?id= used to render an error 'invalid duel link' and a
'back to home' link — a dead-end. Repurpose it as the entry point for
creating a fresh shared grid:

- Landing screen with 'Create a grid to share' CTA. Clicking POSTs
  /api/duels and either fires the Web Share API or falls back to
  clipboard copy.
- A 'Share again' + 'Copy' panel appears once the link is created so
  the player can re-share without rolling a new grid.
- Errors from a broken/expired duel link (401/404/410) now show a
  banner above the create CTA instead of replacing the page — the
  player can immediately spin up a fresh duel.
- 'view' screen (resolved ?id=&sig=) and 'playing' screen (mounted
  Grid) move to a typed 'screen' state machine so the three modes
  are explicit and the markup stays linear.

i18n: 5 new keys (eyebrow, title, subtitle, button, hint) for fr + en.
@Calixteair Calixteair merged commit ac433d9 into main May 13, 2026
7 checks passed
@Calixteair Calixteair deleted the feat/duel-share-and-fix branch May 13, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant