fix(profile): visual frame previews in shop + sidebar avatar sync
Shop items of type 'frame' now render a real avatar-sized preview with
the frame's CSS applied (instead of a generic lucide icon) so buyers
see exactly what they're paying for. Title items get a tag-shaped
preview in their color. The avatar-frames section above the shop also
shows the user's actual avatar inside the frame circles, not 'LS' text.
Sidebar nav-avatar now:
• renders the uploaded avatar_url instead of always showing initials
(LS.initPage + new LS.refreshNavAvatar helper)
• picks up frame CSS on every page via applyCosmetics — previously
only dashboard.html applied it
• repaints immediately after picking/deleting an avatar preset
(avPickPreset / avDelete now call LS.setUser + LS.refreshNavAvatar)
Backend getMyActive resolves avatar_frame to {id, css} for both
gamification frames ('fire', 'crown', ...) and shop-purchased frames
('shop_<id>'), so the client doesn't need a second round-trip to
look up the CSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -707,6 +707,26 @@ function lsModal({ title = '', content = '', size = 'md', actions = [], onClose,
|
||||
return { close, root: ov, body: bodyEl, setBody, setActions, setError };
|
||||
}
|
||||
|
||||
/* ── renderNavAvatar — paint the sidebar avatar (image or initials) ──
|
||||
Exported via LS.refreshNavAvatar so pages that update avatar_url
|
||||
(profile preset picker, upload flow) can re-paint without reload. */
|
||||
function renderNavAvatar(el, user) {
|
||||
if (!el) return;
|
||||
const u = user || getUser();
|
||||
const url = u?.avatar_url;
|
||||
if (url) {
|
||||
el.innerHTML = `<img src="/avatars/${escapeHtml(url)}?t=${Date.now()}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;display:block">`;
|
||||
el.style.background = 'transparent';
|
||||
} else {
|
||||
const initials = (u?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
el.textContent = initials;
|
||||
el.style.background = '';
|
||||
}
|
||||
}
|
||||
function refreshNavAvatar() {
|
||||
renderNavAvatar(document.getElementById('nav-avatar'));
|
||||
}
|
||||
|
||||
/* ── applyRoleSidebar — reveal teacher/admin sidebar items ───────────── */
|
||||
function applyRoleSidebar(user) {
|
||||
if (!user) return;
|
||||
@@ -722,11 +742,11 @@ function initPage({ requireLogin = true } = {}) {
|
||||
const isTeacher = user && ['teacher', 'admin'].includes(user.role);
|
||||
const isAdmin = user?.role === 'admin';
|
||||
|
||||
// Nav avatar
|
||||
// Nav avatar — render uploaded avatar if available, otherwise initials.
|
||||
const navUser = document.getElementById('nav-user');
|
||||
const navAvatar = document.getElementById('nav-avatar');
|
||||
if (navUser) navUser.textContent = user?.name || user?.email || '';
|
||||
if (navAvatar) navAvatar.textContent = (user?.name || 'LS').split(' ').slice(0, 2).map(w => w[0]?.toUpperCase() || '').join('') || 'LS';
|
||||
if (navAvatar) renderNavAvatar(navAvatar, user);
|
||||
|
||||
// Sidebar collapsed state
|
||||
if (localStorage.getItem('ls_sb_collapsed') === '1') {
|
||||
@@ -996,6 +1016,7 @@ window.LS = {
|
||||
del: (path) => apiFetch(path, { method: 'DELETE' }),
|
||||
patch: (path, body) => apiFetch(path, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
applyCosmetics: applyCosmetics,
|
||||
refreshNavAvatar,
|
||||
loadFeatures,
|
||||
clearFeaturesCache,
|
||||
hideDisabledFeatures,
|
||||
@@ -1015,6 +1036,21 @@ async function applyCosmetics() {
|
||||
const c = await getMyActiveCosmetics();
|
||||
if (!c) return;
|
||||
|
||||
// ── Frame: apply CSS to sidebar avatar on every page ──
|
||||
if (c.frame && c.frame.css) {
|
||||
const navAv = document.getElementById('nav-avatar');
|
||||
if (navAv) {
|
||||
// Strip any previously applied frame styles so swaps don't stack.
|
||||
if (navAv.dataset.frameApplied) {
|
||||
navAv.style.cssText = navAv.dataset.frameOrig || '';
|
||||
} else {
|
||||
navAv.dataset.frameOrig = navAv.style.cssText || '';
|
||||
}
|
||||
navAv.style.cssText += ';' + c.frame.css;
|
||||
navAv.dataset.frameApplied = '1';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Title: show under nav username ──
|
||||
if (c.title && c.title.text) {
|
||||
const nameEl = document.getElementById('nav-user');
|
||||
|
||||
Reference in New Issue
Block a user