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
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "image/jpeg")
|
w.Header().Set("Content-Type", "image/jpeg")
|
||||||
w.Write(e.thumb)
|
http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(e.thumb))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", e.mimeType)
|
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) {
|
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>',
|
'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>',
|
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>',
|
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>',
|
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 = [
|
const navSections = [
|
||||||
@@ -362,16 +364,22 @@ function breadcrumbs(parts) {
|
|||||||
return top;
|
return top;
|
||||||
}
|
}
|
||||||
|
|
||||||
function iconButton(name, href) {
|
function iconButton(name, label, action) {
|
||||||
const node = el(href ? 'a' : 'button', 'icon-button');
|
const node = el(typeof action === 'string' ? 'a' : 'button', 'icon-button');
|
||||||
if (href) {
|
if (typeof action === 'string') {
|
||||||
node.href = href;
|
node.href = action;
|
||||||
node.target = '_blank';
|
node.target = '_blank';
|
||||||
|
} else if (action) {
|
||||||
|
node.addEventListener('click', action);
|
||||||
}
|
}
|
||||||
node.append(svg(name));
|
node.append(svg(name), el('span', '', label));
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyButton(text) {
|
||||||
|
return iconButton('copy', 'Copy', () => navigator.clipboard.writeText(text));
|
||||||
|
}
|
||||||
|
|
||||||
function contactRow(value, buttons) {
|
function contactRow(value, buttons) {
|
||||||
const row = el('div', 'contact-row');
|
const row = el('div', 'contact-row');
|
||||||
row.append(value);
|
row.append(value);
|
||||||
@@ -420,6 +428,12 @@ function familyBand(p, family) {
|
|||||||
const grid = el('div', 'band-grid container');
|
const grid = el('div', 'band-grid container');
|
||||||
const left = el('div', 'band-left');
|
const left = el('div', 'band-left');
|
||||||
left.append(el('h2', '', `${p.fullName}'s Family`));
|
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) {
|
if (family.photoCaption) {
|
||||||
left.append(el('div', 'band-caption', 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) {
|
if (p.isStudent && kid.email === p.email) {
|
||||||
continue;
|
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);
|
const adults = (family.adultEmails || []).map(e => byEmail[e]).filter(Boolean).filter(a => a.email !== p.email);
|
||||||
if (adults.length) {
|
if (adults.length) {
|
||||||
right.append(el('div', 'member-header', 'Adults'));
|
right.append(el('div', 'member-header', 'Adults'));
|
||||||
for (const adult of 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');
|
const see = el('a', 'see-family');
|
||||||
@@ -489,9 +505,16 @@ function renderPersonDetail(email) {
|
|||||||
right.append(el('div', 'detail-sub', chain));
|
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), [
|
right.append(contactRow(el('div', 'contact-value', p.email), [
|
||||||
iconButton('message'),
|
copyButton(p.email),
|
||||||
iconButton('mail', 'mailto:' + p.email),
|
iconButton('mail', 'Email', 'mailto:' + p.email),
|
||||||
]));
|
]));
|
||||||
const family = state.model.families[p.familyKey];
|
const family = state.model.families[p.familyKey];
|
||||||
if (family && family.address) {
|
if (family && family.address) {
|
||||||
@@ -499,10 +522,18 @@ function renderPersonDetail(email) {
|
|||||||
block.append(el('div', 'field-label', 'Address'));
|
block.append(el('div', 'field-label', 'Address'));
|
||||||
block.append(el('div', 'contact-value', family.address));
|
block.append(el('div', 'contact-value', family.address));
|
||||||
right.append(contactRow(block, [
|
right.append(contactRow(block, [
|
||||||
iconButton('message'),
|
copyButton(family.address),
|
||||||
iconButton('map', 'https://maps.google.com/?q=' + encodeURIComponent(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);
|
grid.append(right);
|
||||||
content.append(grid);
|
content.append(grid);
|
||||||
|
|
||||||
@@ -561,10 +592,18 @@ function renderFamilyDetail(key) {
|
|||||||
}
|
}
|
||||||
if (family.address) {
|
if (family.address) {
|
||||||
right.append(contactRow(el('div', 'contact-value', family.address), [
|
right.append(contactRow(el('div', 'contact-value', family.address), [
|
||||||
iconButton('message'),
|
copyButton(family.address),
|
||||||
iconButton('map', 'https://maps.google.com/?q=' + encodeURIComponent(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);
|
grid.append(right);
|
||||||
content.append(grid);
|
content.append(grid);
|
||||||
main.append(content);
|
main.append(content);
|
||||||
|
|||||||
@@ -477,7 +477,7 @@ a.family-card {
|
|||||||
|
|
||||||
.detail-grid {
|
.detail-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 302px minmax(0, 1fr);
|
grid-template-columns: minmax(0, 31fr) minmax(0, 69fr);
|
||||||
gap: 28px;
|
gap: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,15 +523,19 @@ a.family-card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 0;
|
|
||||||
background: #f1f3f4;
|
|
||||||
color: #3c4650;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -543,6 +547,32 @@ a.family-card {
|
|||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-linejoin: 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 {
|
.about-header {
|
||||||
@@ -564,8 +594,8 @@ a.family-card {
|
|||||||
|
|
||||||
.band-grid {
|
.band-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: minmax(0, 31fr) minmax(0, 69fr);
|
||||||
gap: 44px;
|
gap: 28px;
|
||||||
padding-top: 36px;
|
padding-top: 36px;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
@@ -577,13 +607,18 @@ a.family-card {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.band-photo {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.band-caption {
|
.band-caption {
|
||||||
color: rgba(255, 255, 255, 0.72);
|
color: rgba(255, 255, 255, 0.72);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 320px;
|
margin-top: 20px;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.band-title {
|
.band-title {
|
||||||
@@ -610,9 +645,9 @@ a.family-card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.member-thumb {
|
.member-thumb {
|
||||||
width: 40px;
|
width: 50px;
|
||||||
height: 40px;
|
height: 50px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user