|
384 | 384 | const niceLink = (u) => (u || '').replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '') |
385 | 385 | const initial = (s) => (s || '?').trim().split(/\s+/).pop().charAt(0).toUpperCase() |
386 | 386 |
|
| 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 | + |
387 | 400 | function ProfileView({ identity }) { |
388 | 401 | const island = readIsland('profile') || {} |
389 | 402 | const webid = identity?.type === 'solid' ? identity.id : null |
390 | 403 | const fetched = useProfileFor(webid) |
391 | 404 |
|
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 |
396 | 410 | 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 | + |
398 | 413 | return html` |
399 | 414 | <div> |
400 | 415 | <div style=${{ marginBottom: '14px', display: 'flex', gap: '8px', alignItems: 'center', fontSize: '12px', color: 'var(--muted)' }}> |
401 | 416 | ${source === 'pod' && html`<span class="island-badge" style=${{ background: '#d1fae5', color: '#065f46' }}>live</span><span>Fetched from <code>${webid}</code></span>`} |
402 | 417 | ${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>`} |
404 | 419 | ${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>`} |
405 | 420 | </div> |
406 | 421 | <div class="two-col"> |
407 | 422 | <div> |
408 | 423 | <div class="card"> |
409 | 424 | <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} /> |
413 | 426 | <div> |
414 | 427 | <h1 class="name"> |
415 | | - <span>${name}</span> |
| 428 | + <span>${name || (isPod ? 'Anonymous' : 'Sample')}</span> |
416 | 429 | ${pronouns && html`<span class="pronoun">(${pronouns})</span>`} |
417 | 430 | </h1> |
418 | 431 | <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" />`)} |
423 | 440 | </div> |
424 | 441 | </div> |
425 | 442 | </div> |
426 | 443 | </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>`)} |
432 | 447 | </div> |
433 | 448 | <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>`)} |
444 | 455 | </div> |
445 | 456 | </div> |
446 | 457 | </div> |
|
0 commit comments