Match detail pages: band photo, contact pills, phone, pronunciation playback
This commit is contained in:
@@ -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) {
|
||||
|
||||
+52
-13
@@ -22,7 +22,9 @@ const icons = {
|
||||
'chevron-right': '<svg viewBox="0 0 24 24"><path d="m9 18 6-6-6-6"/></svg>',
|
||||
heart: '<svg viewBox="0 0 24 24"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg>',
|
||||
mail: '<svg viewBox="0 0 24 24"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>',
|
||||
copy: '<svg viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>',
|
||||
message: '<svg viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>',
|
||||
phone: '<svg viewBox="0 0 24 24"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>',
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user