Skip to content

Commit 745cc40

Browse files
preact 04: pod-only when signed in + Avatar component
- Avatar: useState-driven error fallback. Fix for the 'initial letter sticks through re-render' bug — old code used an onerror handler that DOM-swapped the <img> for a <div>, leaving Preact unable to reconcile when src changed. - Drop the sample overlay when signed in. If the pod's WebID has only name + img (which is common — it does for melvin.me), show just that, with 'Add address' / 'Add phone' placeholder slots for the missing facts. Honest about what's actually in your pod and sets the stage for write-back.
1 parent 5ded35e commit 745cc40

1 file changed

Lines changed: 40 additions & 29 deletions

File tree

preact/04-login/index.html

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -384,63 +384,74 @@
384384
const niceLink = (u) => (u || '').replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')
385385
const initial = (s) => (s || '?').trim().split(/\s+/).pop().charAt(0).toUpperCase()
386386

387+
// useState-driven img with fallback. Avoids DOM-swap closures that break
388+
// across re-renders. If src changes, error state resets.
389+
function Avatar({ src, name }) {
390+
const [errored, setErrored] = useState(false)
391+
useEffect(() => { setErrored(false) }, [src])
392+
if (!src || errored) return html`<div class="photo-fallback">${initial(name)}</div>`
393+
return html`<img class="photo" src=${src} alt=${name} onError=${() => setErrored(true)} />`
394+
}
395+
396+
function EmptyFact({ icon, text }) {
397+
return html`<div class="fact" style=${{ opacity: 0.5, fontStyle: 'italic' }}><span class="ic">${icon}</span><span>${text}</span></div>`
398+
}
399+
387400
function ProfileView({ identity }) {
388401
const island = readIsland('profile') || {}
389402
const webid = identity?.type === 'solid' ? identity.id : null
390403
const fetched = useProfileFor(webid)
391404

392-
// Merge strategy: start from embedded island as a baseline, overlay fetched
393-
// values. That way UI fields the pod doesn't provide (bio, skills, etc.)
394-
// don't vanish — they just remain at sample values for the demo.
395-
const data = fetched.data ? { ...island, ...fetched.data } : island
405+
// Honesty mode: when signed in, show ONLY what the pod returned. Missing
406+
// fields get placeholder slots (not sample leakage). When not signed in,
407+
// render the embedded sample so the page isn't empty on first visit.
408+
const isPod = !!fetched.data
409+
const data = isPod ? fetched.data : island
396410
const { name, nick, pronouns, address, email, phone, homepage, img, bio, skills = [], languages = [] } = data
397-
const source = fetched.data ? 'pod' : (fetched.loading ? 'loading' : (fetched.error ? 'error' : 'sample'))
411+
const source = isPod ? 'pod' : (fetched.loading ? 'loading' : (fetched.error ? 'error' : 'sample'))
412+
398413
return html`
399414
<div>
400415
<div style=${{ marginBottom: '14px', display: 'flex', gap: '8px', alignItems: 'center', fontSize: '12px', color: 'var(--muted)' }}>
401416
${source === 'pod' && html`<span class="island-badge" style=${{ background: '#d1fae5', color: '#065f46' }}>live</span><span>Fetched from <code>${webid}</code></span>`}
402417
${source === 'loading' && html`<span class="island-badge"></span><span>Fetching <code>${webid}</code></span>`}
403-
${source === 'error' && html`<span class="island-badge" style=${{ background: '#fee2e2', color: '#991b1b' }}>error</span><span>${fetched.error}falling back to sample</span>`}
418+
${source === 'error' && html`<span class="island-badge" style=${{ background: '#fee2e2', color: '#991b1b' }}>error</span><span>${fetched.error}showing sample</span>`}
404419
${source === 'sample' && html`<span class="island-badge" style=${{ background: '#fef3c7', color: '#92400e' }}>sample</span><span>Sign in via the login button to load your WebID</span>`}
405420
</div>
406421
<div class="two-col">
407422
<div>
408423
<div class="card">
409424
<div class="hero">
410-
${img
411-
? html`<img class="photo" src=${img} alt=${name} onerror=${(e) => { const f = document.createElement('div'); f.className = 'photo-fallback'; f.textContent = initial(name); e.target.replaceWith(f) }} />`
412-
: html`<div class="photo-fallback">${initial(name)}</div>`}
425+
<${Avatar} src=${img} name=${name} />
413426
<div>
414427
<h1 class="name">
415-
<span>${name}</span>
428+
<span>${name || (isPod ? 'Anonymous' : 'Sample')}</span>
416429
${pronouns && html`<span class="pronoun">(${pronouns})</span>`}
417430
</h1>
418431
<div class="facts">
419-
${address && html`<div class="fact"><span class="ic">\u{1F4CD}</span><span>${address}</span></div>`}
420-
${phone && html`<div class="fact"><span class="ic">\u{260E}\uFE0F</span><a href="tel:${phone.replace(/\s+/g,'')}">${phone}</a></div>`}
421-
${email && html`<div class="fact"><span class="ic">\u{2709}\uFE0F</span><a href="mailto:${email}">${email}</a></div>`}
422-
${homepage && html`<div class="fact"><span class="ic">\u{1F310}</span><a href=${homepage} target="_blank" rel="noopener">${niceLink(homepage)}</a></div>`}
432+
${address ? html`<div class="fact"><span class="ic">\u{1F4CD}</span><span>${address}</span></div>`
433+
: (isPod && html`<${EmptyFact} icon="\u{1F4CD}" text="Add address" />`)}
434+
${phone ? html`<div class="fact"><span class="ic">\u{260E}\uFE0F</span><a href="tel:${phone.replace(/\s+/g,'')}">${phone}</a></div>`
435+
: (isPod && html`<${EmptyFact} icon="\u{260E}\uFE0F" text="Add phone" />`)}
436+
${email ? html`<div class="fact"><span class="ic">\u{2709}\uFE0F</span><a href="mailto:${email}">${email}</a></div>`
437+
: (isPod && html`<${EmptyFact} icon="\u{2709}\uFE0F" text="Add email" />`)}
438+
${homepage ? html`<div class="fact"><span class="ic">\u{1F310}</span><a href=${homepage} target="_blank" rel="noopener">${niceLink(homepage)}</a></div>`
439+
: (isPod && html`<${EmptyFact} icon="\u{1F310}" text="Add homepage" />`)}
423440
</div>
424441
</div>
425442
</div>
426443
</div>
427-
${bio && html`
428-
<div class="card">
429-
<h2 class="section-title">Bio</h2>
430-
<p class="prose">${bio}</p>
431-
</div>`}
444+
${bio
445+
? html`<div class="card"><h2 class="section-title">Bio</h2><p class="prose">${bio}</p></div>`
446+
: (isPod && html`<div class="card"><h2 class="section-title">Bio</h2><p class="prose" style=${{ color: 'var(--muted)', fontStyle: 'italic' }}>No bio in your WebID yet. Stage 5 will add inline editing that writes back to your pod.</p></div>`)}
432447
</div>
433448
<div>
434-
${skills.length > 0 && html`
435-
<div class="card">
436-
<h3 class="side-title">Skills</h3>
437-
<div class="pills">${skills.map(s => html`<span class="pill">${s}</span>`)}</div>
438-
</div>`}
439-
${languages.length > 0 && html`
440-
<div class="card">
441-
<h3 class="side-title">Languages</h3>
442-
<ul class="list-plain">${languages.map(l => html`<li>${l}</li>`)}</ul>
443-
</div>`}
449+
${skills.length > 0
450+
? html`<div class="card"><h3 class="side-title">Skills</h3><div class="pills">${skills.map(s => html`<span class="pill">${s}</span>`)}</div></div>`
451+
: (isPod && html`<div class="card"><h3 class="side-title">Skills</h3><div style=${{ color: 'var(--muted)', fontStyle: 'italic', fontSize: '13px' }}>Not in your WebID</div></div>`)}
452+
${languages.length > 0
453+
? html`<div class="card"><h3 class="side-title">Languages</h3><ul class="list-plain">${languages.map(l => html`<li>${l}</li>`)}</ul></div>`
454+
: (isPod && html`<div class="card"><h3 class="side-title">Languages</h3><div style=${{ color: 'var(--muted)', fontStyle: 'italic', fontSize: '13px' }}>Not in your WebID</div></div>`)}
444455
</div>
445456
</div>
446457
</div>

0 commit comments

Comments
 (0)