import { escapeHtml } from '../util.js'; import { logoLockup } from '../logo.js'; import { renderCornerRail } from '../cornerRail.js'; import { attachTooltips } from '../tooltip.js'; import { renderTopRail } from '../topRail.js'; import { api, ApiError } from '../api.js'; import { FLAIR_ICONS, CLOSE_ICON } from './icons.js'; // modeKey format is set in app.js: "words-25", "time-30", "quote-". function modeLabel(modeKey) { const [kind, arg] = modeKey.split('-'); if (kind === 'words') return `Words · ${arg}`; if (kind === 'time') return `Time · ${arg}s`; if (kind === 'quote') return arg === 'random' ? 'Random' : arg.charAt(0).toUpperCase() + arg.slice(1); return modeKey; } function formatDate(iso) { const t = Date.parse(iso); return Number.isNaN(t) ? '' : new Date(t).toLocaleDateString(); } // Read-only - no settings, no actions on someone else's data, just what // /api/users/:username/public hands back (the same aggregate shape // /api/stats/me uses for yourself, minus anything private). export function renderPublicProfileScreen(root, { username, onBack, onShowStats, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore }) { function bodyMarkup(state) { if (state.status === 'loading') return `Loading...`; if (state.status === 'error') return `${escapeHtml(state.message)}`; const p = state.profile; return ` Tests${p.tests_completed} Avg WPM${Math.round(p.average_wpm)} Best WPM${Math.round(p.best_wpm)} Avg Acc${Math.round(p.average_accuracy)}% Joined ${formatDate(p.joined_at)} Personal bests ${p.personal_bests.length > 0 ? ` ${p.personal_bests.map(b => ` ${escapeHtml(modeLabel(b.mode_key))} ${Math.round(b.wpm)} wpm ${formatDate(b.date)} `).join('')} ` : `No personal bests recorded yet.`} `; } root.innerHTML = ` ${CLOSE_ICON} ${logoLockup()} ${escapeHtml(username)} `; root.querySelectorAll('[data-action="menu"]').forEach(el => el.addEventListener('click', onBack)); attachTooltips(root); const cleanupTheme = renderTopRail(root, { onShowAccount, onShowFriends }); const bodyEl = root.querySelector('.public-profile-body'); const headingEl = root.querySelector('.public-profile-heading'); bodyEl.innerHTML = bodyMarkup({ status: 'loading' }); api.get(`/api/users/${encodeURIComponent(username)}/public`) .then(profile => { bodyEl.innerHTML = bodyMarkup({ status: 'ok', profile }); if (profile.flair && FLAIR_ICONS[profile.flair]) { headingEl.innerHTML = `${escapeHtml(username)}${FLAIR_ICONS[profile.flair]}`; // Scoped to headingEl, not the whole root - see the same // note in leaderboardScreen.js's load(). attachTooltips(headingEl); } }) .catch(err => { const message = err instanceof ApiError && err.status === 404 ? `No profile found for "${username}".` : 'Could not load this profile - try again.'; bodyEl.innerHTML = bodyMarkup({ status: 'error', message }); }); const cleanupRail = renderCornerRail(root, { onShowStats, onShowPlaceholder, onShowAccount, onShowLeaderboard, onShowFriends, onShowMultiplayer, onShowStore }); return () => { cleanupTheme(); cleanupRail(); }; }