diff --git a/static/js/09-profile.js b/static/js/09-profile.js index dcdef31..2762106 100644 --- a/static/js/09-profile.js +++ b/static/js/09-profile.js @@ -1,6 +1,5 @@ // static/js/09-profile.js "use strict"; - window.Glow = window.Glow || {}; Glow.escapeHtml = value => @@ -13,16 +12,13 @@ Glow.escapeHtml = value => Glow.renderProfileBody = profile => { const wrap = document.createElement("div"); wrap.className = "profile-rendered"; - const content = String(profile?.bio_html || "").trim(); - if (!content) { const empty = document.createElement("div"); empty.className = "profile-empty"; empty.textContent = "No profile bio yet."; return empty; } - wrap.innerHTML = content; return wrap; }; @@ -31,11 +27,9 @@ Glow.openProfile = async username => { Glow.state.activeView = "profile"; Glow.state.activeProfile = null; Glow.updateViewUI(); - const box = Glow.els.profileBox; box.setAttribute("aria-busy", "true"); box.replaceChildren(); - try { const data = await Glow.apiGetPublicProfile(username); Glow.state.activeProfile = data.profile; @@ -44,14 +38,11 @@ Glow.openProfile = async username => { box.replaceChildren(); const state = document.createElement("div"); state.className = "empty-state"; - const card = document.createElement("div"); card.className = "empty-card"; card.textContent = error.message || "Could not load profile."; - state.append(card); box.append(state); - Glow.showToast(error.message || "Could not load profile.", "error"); } finally { box.setAttribute("aria-busy", "false"); @@ -63,15 +54,12 @@ Glow.openMyProfile = async (edit = false) => { Glow.showToast("Sign in to view your profile.", "error"); return; } - Glow.state.activeView = "profile"; Glow.state.activeProfile = null; Glow.updateViewUI(); - const box = Glow.els.profileBox; box.setAttribute("aria-busy", "true"); box.replaceChildren(); - try { const data = await Glow.apiGetMyProfile(); Glow.state.activeProfile = data.profile; @@ -83,30 +71,45 @@ Glow.openMyProfile = async (edit = false) => { } }; +Glow.uploadBannerForProfile = async file => { + const result = await Glow.uploadAttachment(file); + return result?.attachment || null; +}; + Glow.renderProfileView = (editing = false) => { const box = Glow.els.profileBox; const profile = Glow.state.activeProfile; - box.replaceChildren(); if (!profile) { const state = document.createElement("div"); state.className = "empty-state"; - const card = document.createElement("div"); card.className = "empty-card"; card.textContent = "Profile not found."; - state.append(card); box.append(state); return; } + box.style.background = + profile.profile_background_color || "transparent"; + const card = document.createElement("div"); card.className = "profile-card"; const banner = document.createElement("div"); banner.className = "profile-banner"; + if (profile.profile_banner_color) { + banner.style.background = + `linear-gradient(135deg, ${profile.profile_banner_color}, transparent 72%), ${profile.profile_banner_color}`; + } + if (profile.banner_image_url) { + banner.style.backgroundImage = + `linear-gradient(135deg, rgba(0,0,0,0.18), rgba(0,0,0,0.04)), url("${profile.banner_image_url}")`; + banner.style.backgroundSize = "cover"; + banner.style.backgroundPosition = "center"; + } const content = document.createElement("div"); content.className = "profile-content"; @@ -122,7 +125,6 @@ Glow.renderProfileView = (editing = false) => { const actions = document.createElement("div"); actions.className = "profile-actions"; - if (profile.is_self) { const editButton = document.createElement("button"); editButton.className = "button"; @@ -139,10 +141,8 @@ Glow.renderProfileView = (editing = false) => { settingsButton.addEventListener("click", () => { Glow.openSettings?.(); }); - actions.append(editButton, settingsButton); } - header.append(avatar, actions); const name = document.createElement("div"); @@ -172,7 +172,11 @@ Glow.renderProfileView = (editing = false) => { statusBadge.className = "profile-badge"; statusBadge.textContent = profile.status; - badges.append(roleBadge, statusBadge); + const presenceBadge = document.createElement("span"); + presenceBadge.className = "profile-badge"; + presenceBadge.textContent = profile.presence_mode || "online"; + + badges.append(roleBadge, statusBadge, presenceBadge); const grid = document.createElement("div"); grid.className = "profile-grid"; @@ -182,38 +186,38 @@ Glow.renderProfileView = (editing = false) => { } else { const bioSection = document.createElement("section"); bioSection.className = "profile-section"; - const bioTitle = document.createElement("h3"); bioTitle.textContent = "Bio"; - bioSection.append(bioTitle, Glow.renderProfileBody(profile)); const metaSection = document.createElement("section"); metaSection.className = "profile-section"; - const metaTitle = document.createElement("h3"); metaTitle.textContent = "Profile"; - const metaList = document.createElement("dl"); metaList.className = "profile-meta-list"; const addMeta = (label, value) => { const dt = document.createElement("dt"); dt.textContent = label; - const dd = document.createElement("dd"); dd.textContent = value || "—"; - metaList.append(dt, dd); }; addMeta("Display name", profile.display_name || profile.username); + addMeta("Status message", profile.status_message || ""); + addMeta("Presence mode", profile.presence_mode || "online"); addMeta("Website", profile.website || ""); addMeta("Location", profile.location || ""); addMeta( "Joined", profile.created_at ? new Date(profile.created_at).toLocaleDateString() : "" ); + addMeta( + "Last active", + profile.last_active_at ? new Date(profile.last_active_at).toLocaleString() : "" + ); addMeta("Bio mode", profile.bio_mode || "txt"); addMeta("Name color", profile.name_color || "role default"); @@ -221,7 +225,16 @@ Glow.renderProfileView = (editing = false) => { grid.append(bioSection, metaSection); } - content.append(header, name, handle, badges, grid); + content.append(header, name, handle); + + if (profile.status_message) { + const statusMessage = document.createElement("div"); + statusMessage.className = "profile-status-message"; + statusMessage.textContent = profile.status_message; + content.append(statusMessage); + } + + content.append(badges, grid); card.append(banner, content); box.append(card); }; @@ -229,7 +242,6 @@ Glow.renderProfileView = (editing = false) => { Glow.buildProfileEditor = profile => { const section = document.createElement("section"); section.className = "profile-section"; - const title = document.createElement("h3"); title.textContent = "Edit profile"; @@ -243,6 +255,13 @@ Glow.buildProfileEditor = profile => { displayName.placeholder = "Display name"; displayName.value = profile.display_name || ""; + const statusMessage = document.createElement("input"); + statusMessage.className = "field"; + statusMessage.type = "text"; + statusMessage.maxLength = 160; + statusMessage.placeholder = "Status message"; + statusMessage.value = profile.status_message || ""; + const website = document.createElement("input"); website.className = "field"; website.type = "url"; @@ -257,9 +276,13 @@ Glow.buildProfileEditor = profile => { location.placeholder = "Location"; location.value = profile.location || ""; + const bannerInput = document.createElement("input"); + bannerInput.className = "field"; + bannerInput.type = "file"; + bannerInput.accept = ".png,.jpg,.jpeg,.gif,.webp"; + const modeRow = document.createElement("div"); modeRow.className = "profile-mode-row"; - let selectedMode = profile.bio_mode || "txt"; const body = document.createElement("textarea"); @@ -273,14 +296,12 @@ Glow.buildProfileEditor = profile => { button.className = `mode-pill${selectedMode === mode ? " active" : ""}`; button.type = "button"; button.textContent = mode.toUpperCase(); - button.addEventListener("click", () => { selectedMode = mode; for (const el of modeRow.querySelectorAll(".mode-pill")) { el.classList.toggle("active", el.textContent.toLowerCase() === mode); } }); - modeRow.append(button); } @@ -298,17 +319,28 @@ Glow.buildProfileEditor = profile => { save.textContent = "Save profile"; save.addEventListener("click", async () => { try { + let bannerAttachmentId = profile.banner_attachment_id || null; + const file = bannerInput.files?.[0]; + if (file) { + const uploaded = await Glow.uploadBannerForProfile(file); + bannerAttachmentId = uploaded?.id || bannerAttachmentId; + } + const data = await Glow.apiUpdateMyProfile({ display_name: displayName.value, + status_message: statusMessage.value, website: website.value, location: location.value, bio: body.value, - bio_mode: selectedMode + bio_mode: selectedMode, + banner_attachment_id: bannerAttachmentId }); Glow.state.activeProfile = { ...data.profile, - name_color: Glow.state.settings?.name_color || profile.name_color || null + name_color: Glow.state.settings?.name_color || profile.name_color || null, + profile_background_color: Glow.state.settings?.profile_background_color || null, + profile_banner_color: Glow.state.settings?.profile_banner_color || null }; Glow.showToast("Profile updated."); Glow.renderProfileView(false); @@ -323,17 +355,28 @@ Glow.buildProfileEditor = profile => { preview.textContent = "Preview"; preview.addEventListener("click", async () => { try { + let bannerAttachmentId = profile.banner_attachment_id || null; + const file = bannerInput.files?.[0]; + if (file) { + const uploaded = await Glow.uploadBannerForProfile(file); + bannerAttachmentId = uploaded?.id || bannerAttachmentId; + } + const data = await Glow.apiUpdateMyProfile({ display_name: displayName.value, + status_message: statusMessage.value, website: website.value, location: location.value, bio: body.value, - bio_mode: selectedMode + bio_mode: selectedMode, + banner_attachment_id: bannerAttachmentId }); Glow.state.activeProfile = { ...data.profile, - name_color: Glow.state.settings?.name_color || profile.name_color || null + name_color: Glow.state.settings?.name_color || profile.name_color || null, + profile_background_color: Glow.state.settings?.profile_background_color || null, + profile_banner_color: Glow.state.settings?.profile_banner_color || null }; Glow.renderProfileView(false); } catch (error) { @@ -342,8 +385,17 @@ Glow.buildProfileEditor = profile => { }); actions.append(save, preview); - editor.append(displayName, website, location, modeRow, help, body, actions); + editor.append( + displayName, + statusMessage, + website, + location, + bannerInput, + modeRow, + help, + body, + actions + ); section.append(title, editor); - return section; -}; +}; \ No newline at end of file