From 877c9895396d4aa3bab63d7d07f973f3a597a57c Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sat, 15 Aug 2026 21:35:15 -0700 Subject: [PATCH] Match detail pages: band photo, contact pills, phone, pronunciation playback --- internal/blob/blob.go | 4 +-- web/static/directory/app.js | 65 +++++++++++++++++++++++++++------- web/static/directory/style.css | 65 ++++++++++++++++++++++++++-------- 3 files changed, 104 insertions(+), 30 deletions(-) diff --git a/internal/blob/blob.go b/internal/blob/blob.go index 423014c..cbc9034 100644 --- a/internal/blob/blob.go +++ b/internal/blob/blob.go @@ -233,11 +233,11 @@ func (s *Store) serve(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "image/jpeg") - w.Write(e.thumb) + http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(e.thumb)) return } w.Header().Set("Content-Type", e.mimeType) - w.Write(e.data) + http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(e.data)) } func thumbnail(src []byte) ([]byte, error) { diff --git a/web/static/directory/app.js b/web/static/directory/app.js index 7a8e572..7a1d346 100644 --- a/web/static/directory/app.js +++ b/web/static/directory/app.js @@ -22,7 +22,9 @@ const icons = { 'chevron-right': '', heart: '', mail: '', + copy: '', message: '', + phone: '', }; const navSections = [ @@ -362,16 +364,22 @@ function breadcrumbs(parts) { return top; } -function iconButton(name, href) { - const node = el(href ? 'a' : 'button', 'icon-button'); - if (href) { - node.href = href; +function iconButton(name, label, action) { + const node = el(typeof action === 'string' ? 'a' : 'button', 'icon-button'); + if (typeof action === 'string') { + node.href = action; node.target = '_blank'; + } else if (action) { + node.addEventListener('click', action); } - node.append(svg(name)); + node.append(svg(name), el('span', '', label)); return node; } +function copyButton(text) { + return iconButton('copy', 'Copy', () => navigator.clipboard.writeText(text)); +} + function contactRow(value, buttons) { const row = el('div', 'contact-row'); row.append(value); @@ -420,6 +428,12 @@ function familyBand(p, family) { const grid = el('div', 'band-grid container'); const left = el('div', 'band-left'); left.append(el('h2', '', `${p.fullName}'s Family`)); + if (family.photoUrl && !p.isStudent) { + const img = el('img', 'band-photo'); + img.src = thumbUrl(family.photoUrl); + img.alt = ''; + left.append(img); + } if (family.photoCaption) { left.append(el('div', 'band-caption', family.photoCaption)); } @@ -433,14 +447,16 @@ function familyBand(p, family) { if (p.isStudent && kid.email === p.email) { continue; } - right.append(memberRow(kid, gradeChain(kid).toUpperCase(), kid.email)); + const label = kid.pronouns ? `${gradeChain(kid)} (${kid.pronouns})` : gradeChain(kid); + right.append(memberRow(kid, label.toUpperCase(), kid.email)); } } const adults = (family.adultEmails || []).map(e => byEmail[e]).filter(Boolean).filter(a => a.email !== p.email); if (adults.length) { right.append(el('div', 'member-header', 'Adults')); for (const adult of adults) { - right.append(memberRow(adult, adult.pronouns ? `(${adult.pronouns.toUpperCase()})` : '', adult.email)); + const label = [adult.phone, adult.pronouns ? `(${adult.pronouns.toUpperCase()})` : ''].filter(Boolean).join(' '); + right.append(memberRow(adult, label, adult.email)); } } const see = el('a', 'see-family'); @@ -489,9 +505,16 @@ function renderPersonDetail(email) { right.append(el('div', 'detail-sub', chain)); } } + if (p.phone) { + right.append(contactRow(el('div', 'contact-value', p.phone), [ + copyButton(p.phone), + iconButton('message', 'Text', 'sms:' + p.phone), + iconButton('phone', 'Call', 'tel:' + p.phone), + ])); + } right.append(contactRow(el('div', 'contact-value', p.email), [ - iconButton('message'), - iconButton('mail', 'mailto:' + p.email), + copyButton(p.email), + iconButton('mail', 'Email', 'mailto:' + p.email), ])); const family = state.model.families[p.familyKey]; if (family && family.address) { @@ -499,10 +522,18 @@ function renderPersonDetail(email) { block.append(el('div', 'field-label', 'Address')); block.append(el('div', 'contact-value', family.address)); right.append(contactRow(block, [ - iconButton('message'), - iconButton('map', 'https://maps.google.com/?q=' + encodeURIComponent(family.address)), + copyButton(family.address), + iconButton('map', 'Map', 'https://maps.google.com/?q=' + encodeURIComponent(family.address)), ])); } + if (p.pronunciationUrl) { + right.append(el('div', 'pronounce-label', 'How do I pronounce this?')); + const audio = el('audio', 'pronounce-player'); + audio.controls = true; + audio.preload = 'metadata'; + audio.src = p.pronunciationUrl; + right.append(audio); + } grid.append(right); content.append(grid); @@ -561,10 +592,18 @@ function renderFamilyDetail(key) { } if (family.address) { right.append(contactRow(el('div', 'contact-value', family.address), [ - iconButton('message'), - iconButton('map', 'https://maps.google.com/?q=' + encodeURIComponent(family.address)), + copyButton(family.address), + iconButton('map', 'Map', 'https://maps.google.com/?q=' + encodeURIComponent(family.address)), ])); } + if (family.pronunciationUrl) { + right.append(el('div', 'pronounce-label', 'How do I pronounce this?')); + const audio = el('audio', 'pronounce-player'); + audio.controls = true; + audio.preload = 'metadata'; + audio.src = family.pronunciationUrl; + right.append(audio); + } grid.append(right); content.append(grid); main.append(content); diff --git a/web/static/directory/style.css b/web/static/directory/style.css index e5c26b1..8f0e053 100644 --- a/web/static/directory/style.css +++ b/web/static/directory/style.css @@ -477,7 +477,7 @@ a.family-card { .detail-grid { display: grid; - grid-template-columns: 302px minmax(0, 1fr); + grid-template-columns: minmax(0, 31fr) minmax(0, 69fr); gap: 28px; } @@ -523,15 +523,19 @@ a.family-card { } .icon-button { - width: 40px; - height: 40px; - border-radius: 50%; - border: 0; - background: #f1f3f4; - color: #3c4650; display: flex; align-items: center; - justify-content: center; + gap: 9px; + height: 44px; + padding: 0 20px; + border-radius: 22px; + border: 0; + background: #f1f3f4; + color: #1f2933; + font: inherit; + font-size: 14px; + font-weight: 600; + text-decoration: none; cursor: pointer; } @@ -543,6 +547,32 @@ a.family-card { stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; + flex-shrink: 0; +} + +@media (max-width: 1399px) { + .icon-button { + width: 40px; + height: 40px; + padding: 0; + justify-content: center; + border-radius: 50%; + } + .icon-button span { + display: none; + } +} + +.pronounce-label { + text-align: center; + color: var(--muted); + font-size: 14px; + margin: 34px 0 12px; +} + +.pronounce-player { + width: 100%; + display: block; } .about-header { @@ -564,8 +594,8 @@ a.family-card { .band-grid { display: grid; - grid-template-columns: 1fr 1fr; - gap: 44px; + grid-template-columns: minmax(0, 31fr) minmax(0, 69fr); + gap: 28px; padding-top: 36px; padding-bottom: 40px; } @@ -577,13 +607,18 @@ a.family-card { text-align: center; } +.band-photo { + width: 100%; + border-radius: 8px; + display: block; +} + .band-caption { color: rgba(255, 255, 255, 0.72); font-size: 13px; line-height: 1.5; text-align: center; - max-width: 320px; - margin: 0 auto; + margin-top: 20px; } .band-title { @@ -610,9 +645,9 @@ a.family-card { } .member-thumb { - width: 40px; - height: 40px; - border-radius: 6px; + width: 50px; + height: 50px; + border-radius: 8px; object-fit: cover; flex-shrink: 0; }