fix(web): broken duel link + create-duel CTA on /duel landing#59
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two related changes around the duel flow:
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;
}
```
Create CTA (`DuelView.svelte`)
Refactor the component around a typed `screen` state machine:
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
Test plan