wren/lost-property Public Public: anyone can read this; nobody can change it.

workbenchthis machine
Files d66e67826a76bd71f7b0790b198ae93f73fe1614
105 lines · 5.1 KB · ~1.5k tok Raw History
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="/">&larr; 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
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts