wren/lost-property Public Public: anyone can read this; nobody can change it.
workbenchthis machine
Files fe26fca1242e118d0a6a72d88af55fc09071adcf
| 1 | /** The two pages: the entrance (catalogue and intake form) and a case. */ |
| 2 | |
| 3 | import { escape } from "./html.ts"; |
| 4 | import { catalogueNumber, conditionOf, vitrineOf, VITRINES } from "./museum.ts"; |
| 5 | import type { Item, ItemWithCount, Note } from "./db.ts"; |
| 6 | |
| 7 | function layout(env: string, commit: string, body: string): string { |
| 8 | return `<!doctype html> |
| 9 | <meta charset="utf-8"> |
| 10 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 11 | <title>lost-property</title> |
| 12 | <style> |
| 13 | :root { color-scheme: light dark; font: 16px/1.5 system-ui, sans-serif; } |
| 14 | body { max-width: 40rem; margin: 3rem auto; padding: 0 1rem; } |
| 15 | .env { display: inline-block; padding: .1rem .5rem; border-radius: 999px; font-size: .8rem; |
| 16 | background: ${env === "production" ? "#16a34a" : "#2563eb"}; color: white; } |
| 17 | textarea, input[type=text] { width: 100%; box-sizing: border-box; padding: .6rem .8rem; font: inherit; |
| 18 | border: 1px solid #8884; border-radius: .5rem; } |
| 19 | button { margin-top: .6rem; padding: .7rem 1.2rem; font: inherit; border: 0; border-radius: .5rem; |
| 20 | background: #111; color: white; min-height: 44px; } |
| 21 | a { color: inherit; } |
| 22 | ul.catalogue { padding: 0; } |
| 23 | li.case { padding: .8rem 0; border-top: 1px solid #8883; list-style: none; } |
| 24 | .tag { display: inline-block; font-size: .75rem; opacity: .7; border: 1px solid #8886; border-radius: 999px; |
| 25 | padding: .05rem .5rem; margin: .3rem .4rem 0 0; } |
| 26 | .plaque { border: 1px solid #8886; border-radius: .5rem; padding: 1rem 1.2rem; margin: 1.5rem 0; } |
| 27 | .plaque .catalogue-number { font-size: .8rem; opacity: .7; letter-spacing: .05em; text-transform: uppercase; } |
| 28 | .plaque h2 { margin: .2rem 0 .8rem; } |
| 29 | .plaque dl { display: grid; grid-template-columns: auto 1fr; gap: .2rem 1rem; font-size: .9rem; opacity: .85; margin: 0; } |
| 30 | .plaque dt { font-weight: 600; } |
| 31 | .plaque dd { margin: 0; } |
| 32 | .note { padding: .6rem 0; border-top: 1px solid #8883; } |
| 33 | .note .kind { font-weight: 600; font-size: .85rem; } |
| 34 | small { opacity: .6; } |
| 35 | fieldset { border: 0; padding: 0; margin: 1rem 0 .8rem; display: flex; gap: 1rem; flex-wrap: wrap; } |
| 36 | label.option { display: flex; align-items: center; gap: .4rem; min-height: 44px; } |
| 37 | </style> |
| 38 | ${body} |
| 39 | <p><small>lost-property <span class="env">${escape(env)}</span> ${commit ? "@ " + escape(commit) : ""}</small></p> |
| 40 | `; |
| 41 | } |
| 42 | |
| 43 | export function renderHome(items: ItemWithCount[], env: string, commit: string): string { |
| 44 | const rows = items |
| 45 | .map((item) => { |
| 46 | const vitrine = vitrineOf(item.description); |
| 47 | const short = item.description.length > 80 ? item.description.slice(0, 80) + "…" : item.description; |
| 48 | return `<li class="case"> |
| 49 | <a href="/items/${item.id}">${escape(short)}</a><br> |
| 50 | <span class="tag">${escape(catalogueNumber(item.id, item.created_at))}</span><span class="tag">${escape(VITRINES[vitrine]!)}</span> |
| 51 | <br><small>${item.note_count} note${item.note_count === 1 ? "" : "s"} · accessioned ${escape(item.created_at)}</small> |
| 52 | </li>`; |
| 53 | }) |
| 54 | .join(""); |
| 55 | return layout( |
| 56 | env, |
| 57 | commit, |
| 58 | `<h1>lost-property</h1> |
| 59 | <p>A museum of lost things. Describe what you lost; the museum accessions it — a catalogue number, a vitrine, a plaque written in the flat voice of a museum label. Other visitors can leave a note on any case: that they saw one like it, or that they have the other half.</p> |
| 60 | <form method="post" action="/items"> |
| 61 | <label for="description">Describe what you lost</label> |
| 62 | <textarea id="description" name="description" rows="3" maxlength="500" required |
| 63 | placeholder="A single brown leather glove, left hand, found beneath a train seat."></textarea> |
| 64 | <button>Accession it</button> |
| 65 | </form> |
| 66 | <ul class="catalogue">${rows || `<li class="case"><small>Nothing accessioned yet.</small></li>`}</ul>`, |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | export function renderCase(item: Item, notes: Note[], env: string, commit: string): string { |
| 71 | const vitrineIndex = vitrineOf(item.description); |
| 72 | const noteRows = notes |
| 73 | .map( |
| 74 | (n) => `<div class="note"> |
| 75 | <span class="kind">${n.kind === "match" ? "Has the other half" : "Saw one like it"}</span> |
| 76 | <p>${escape(n.body)}</p> |
| 77 | <small>${escape(n.created_at)}</small> |
| 78 | </div>`, |
| 79 | ) |
| 80 | .join(""); |
| 81 | return layout( |
| 82 | env, |
| 83 | commit, |
| 84 | `<p><a href="/">← lost-property</a></p> |
| 85 | <article class="plaque"> |
| 86 | <p class="catalogue-number">${escape(catalogueNumber(item.id, item.created_at))} · Vitrine ${vitrineIndex + 1}</p> |
| 87 | <h2>${escape(VITRINES[vitrineIndex]!)}</h2> |
| 88 | <p>${escape(item.description)}</p> |
| 89 | <dl> |
| 90 | <dt>Accessioned</dt><dd>${escape(item.created_at)}</dd> |
| 91 | <dt>Condition</dt><dd>${escape(conditionOf(item.id))}</dd> |
| 92 | </dl> |
| 93 | </article> |
| 94 | <h3>Notes</h3> |
| 95 | ${noteRows || `<p><small>No notes yet.</small></p>`} |
| 96 | <form method="post" action="/items/${item.id}/notes"> |
| 97 | <fieldset> |
| 98 | <label class="option"><input type="radio" name="kind" value="sighting" checked required> I saw one like it</label> |
| 99 | <label class="option"><input type="radio" name="kind" value="match" required> I have the other half</label> |
| 100 | </fieldset> |
| 101 | <input type="text" name="body" maxlength="500" required placeholder="Where, when, or how to get in touch"> |
| 102 | <button>Leave a note</button> |
| 103 | </form>`, |
| 104 | ); |
| 105 | } |