Public: anyone can read this; the crew can change it.
The duplicate-address notice says "open it, or launch another anyway" with both choices inside it #82
Check · Review · Land · Live done — Landed and live in production.
mason/listing-launch:mason/80-the-duplicate-address-notice-says-open-i → main 1 commit fork
4 files
changed +29
−8
docs/design/areas/duplicate.md added +4
| @@ -0,0 +1,4 @@ | ||
| 1 | # Duplicate-address notice | |
| 2 | ||
| 3 | - `link-button`: a submit button styled as an inline link, so a choice can sit inside a sentence ("launch another anyway"). | |
| 4 | - The existing `sr` class keeps a default Launch button first in the form, so Enter in a field never submits `again`. |
src/duplicate.test.ts +10 −2
| @@ -51,10 +51,18 @@ test("the author is asked first: the form comes back with every value, the link | ||
| 51 | 51 | const page = await r.text(); |
| 52 | 52 | expect(page).toContain("You launched 412 Alder Lane on"); |
| 53 | 53 | expect(page).toContain(`href="/listings/${first}"`); |
| 54 | expect(page).toContain('name="again" value="1"'); | |
| 54 | const notice = page.slice(page.indexOf('<div class="card read-only"'), page.indexOf("</div>", page.indexOf('<div class="card read-only"'))); | |
| 55 | expect(notice.replace(/<[^>]*>/g, "")).toContain("You launched 412 Alder Lane on"); | |
| 56 | expect(notice.replace(/<[^>]*>/g, "")).toMatch(/^You launched 412 Alder Lane on [A-Z][a-z]{2} \d{1,2} — open it, or launch another anyway/); | |
| 57 | expect(notice).toContain(`<a href="/listings/${first}">open it</a>`); | |
| 58 | expect(notice).toMatch(/<button class="link-button" name="again" value="1">launch another anyway<\/button>/); | |
| 59 | expect(notice).toContain("Choose the photos again"); | |
| 55 | 60 | // Enter in a field submits with the form's first submit button: it must not be the again one. |
| 56 | const form = page.slice(page.indexOf('<form class="card form"')); | |
| 61 | const form = page.slice(page.indexOf('<form class="card form"'), page.indexOf("</form>")); | |
| 57 | 62 | expect(form.match(/<button[^>]*>/)![0]).not.toContain("again"); |
| 63 | // No second button after Launch. | |
| 64 | expect(form.slice(form.indexOf('class="button button-launch"'))).not.toContain("<button"); | |
| 65 | expect(form.match(/name="again"/g)!.length).toBe(1); | |
| 58 | 66 | expect(page).toContain('value="Ann <b>"'); |
| 59 | 67 | expect(photos()).toBe(before); |
| 60 | 68 | const again = await post("ann", "412 Alder Lane", { again: "1" }); |
src/pages/home.ts +4 −6
| @@ -125,11 +125,9 @@ export function form( | ||
| 125 | 125 | const again = |
| 126 | 126 | duplicate === undefined |
| 127 | 127 | ? "" |
| 128 | : `<p class="card read-only" role="status">You launched ${escape(duplicate.address)} on ${escape(new Date(duplicate.created_at).toLocaleDateString("en", { month: "short", day: "numeric" }))} — <a href="/listings/${duplicate.id}">open it</a>, or launch another anyway with the button after Launch <small class="muted">(choose any photos again)</small></p>`; | |
| 129 | const anyway = | |
| 130 | duplicate === undefined | |
| 131 | ? "" | |
| 132 | : `<button class="button button-quiet" name="again" value="1">Launch another anyway</button>`; | |
| 128 | : `<button class="sr" tabindex="-1" aria-hidden="true">Launch</button> | |
| 129 | <div class="card read-only" role="status"><p>You launched ${escape(duplicate.address)} on ${escape(new Date(duplicate.created_at).toLocaleDateString("en", { month: "short", day: "numeric" }))} — <a href="/listings/${duplicate.id}">open it</a>, or <button class="link-button" name="again" value="1">launch another anyway</button></p> | |
| 130 | <p class="muted"><small>Choose the photos again: a browser cannot keep the files you chose.</small></p></div>`; | |
| 133 | 131 | const launch = |
| 134 | 132 | editing !== undefined |
| 135 | 133 | ? `<button class="button button-launch">Save and write all again</button> |
| @@ -137,7 +135,7 @@ export function form( | ||
| 137 | 135 | <span class="muted">Every asset is written again from these facts: your edits are replaced and every approval is cleared.</span>` |
| 138 | 136 | : who.role === "anon" |
| 139 | 137 | ? `<a class="button" href="/__forge/signin?to=/">Sign in to launch</a>` |
| 140 | : `<button class="button button-launch">Launch</button>${anyway}`; | |
| 138 | : `<button class="button button-launch">Launch</button>`; | |
| 141 | 139 | const photos = `<label class="field"><span>Photos <small class="muted">up to ${PHOTOS_MAX}, JPEG, PNG or WebP, 24 MB in all; the first is the hero</small></span> |
| 142 | 140 | <input class="input file" type="file" name="photos" multiple accept="image/jpeg,image/png,image/webp"></label>`; |
| 143 | 141 | const headshot = `<label class="field"><span>Headshot <small class="muted">${headshotFrom === undefined ? "optional; your last one carries over" : `Using your headshot from ${escape(headshotFrom.address)} — choose a file to replace it`}</small></span> |
static/app.css +11
| @@ -1277,3 +1277,14 @@ main:focus { | ||
| 1277 | 1277 | break-inside: avoid; |
| 1278 | 1278 | } |
| 1279 | 1279 | } |
| 1280 | .link-button { | |
| 1281 | font: inherit; | |
| 1282 | color: var(); | |
| 1283 | background: none; | |
| 1284 | border: 0; | |
| 1285 | padding: 0; | |
| 1286 | text-decoration: underline; | |
| 1287 | text-decoration-thickness: 1px; | |
| 1288 | text-underline-offset: 0.15em; | |
| 1289 | cursor: pointer; | |
| 1290 | } |