diff --git a/static/js/09-profile.js b/static/js/09-profile.js index d810161..39f755c 100644 --- a/static/js/09-profile.js +++ b/static/js/09-profile.js @@ -1,11 +1,6 @@ // static/js/09-profile.js "use strict"; window.Glow = window.Glow || {}; -const showPresenceModeMeta = false; - -if (showPresenceModeMeta) { - addMeta("Presence mode", profile.presence_mode || "online"); -} Glow.escapeHtml = value => String(value ?? "") @@ -28,6 +23,135 @@ Glow.renderProfileBody = profile => { return wrap; }; +Glow.profileCanShowPresenceBadge = profile => { + if (!profile) return false; + if (profile.is_self) return true; + if (typeof profile.show_presence_badge === "boolean") { + return profile.show_presence_badge; + } + return Glow.state.profilePresenceBadgeVisible !== false; +}; + +Glow.profileWebsiteMeta = website => { + return Glow.websiteBrandMeta?.(website) || { + host: "", + brand: "web", + label: "website", + iconText: "â—‹" + }; +}; + +Glow.profileOpenMediaPicker = kind => { + if (kind === "avatar") { + Glow.els.avatarUploadInput?.click(); + return; + } + if (kind === "banner") { + Glow.els.bannerUploadInput?.click(); + } +}; + +Glow.profileResetCropState = () => { + const crop = Glow.state.imageCrop; + if (crop.previewUrl) { + try { + URL.revokeObjectURL(crop.previewUrl); + } catch (_) {} + } + + Glow.state.imageCrop = { + open: false, + kind: "", + file: null, + previewUrl: "", + zoom: 1, + autoCrop: false + }; + + if (Glow.els.imageCropOverlay) { + Glow.els.imageCropOverlay.classList.add("hidden"); + } + if (Glow.els.imageCropPreview) { + Glow.els.imageCropPreview.replaceChildren(); + } + if (Glow.els.imageCropZoom) { + Glow.els.imageCropZoom.value = "1"; + } + if (Glow.els.imageCropAuto) { + Glow.els.imageCropAuto.checked = false; + } +}; + +Glow.profileOpenCropDialog = (kind, file) => { + if (!file || !kind) return; + + Glow.profileResetCropState(); + + const previewUrl = URL.createObjectURL(file); + Glow.state.imageCrop = { + open: true, + kind, + file, + previewUrl, + zoom: 1, + autoCrop: false + }; + + if (Glow.els.imageCropPreview) { + const image = document.createElement("img"); + image.src = previewUrl; + image.alt = `${kind} preview`; + image.className = "crop-preview-image"; + Glow.els.imageCropPreview.replaceChildren(image); + } + + Glow.els.imageCropOverlay?.classList.remove("hidden"); +}; + +Glow.profileApplyCropSelection = () => { + const crop = Glow.state.imageCrop; + if (!crop?.file || !crop?.kind) { + Glow.profileResetCropState(); + return; + } + + if (crop.kind === "avatar") { + Glow.state.profileMediaDraft.avatarFile = crop.file; + } else if (crop.kind === "banner") { + Glow.state.profileMediaDraft.bannerFile = crop.file; + } + + Glow.showToast( + crop.kind === "avatar" + ? "Avatar image selected. Save profile changes to keep it." + : "Banner image selected. Save profile changes to keep it." + ); + + Glow.profileResetCropState(); +}; + +Glow.profileBuildWebsiteBadge = website => { + if (!website) return null; + + const meta = Glow.profileWebsiteMeta(website); + const link = document.createElement("a"); + link.className = `profile-website-badge brand-${meta.brand}`; + link.href = website; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + + const icon = document.createElement("span"); + icon.className = "profile-website-badge-icon"; + icon.textContent = meta.iconText; + + const text = document.createElement("span"); + text.className = "profile-website-badge-text"; + text.textContent = meta.label; + + link.append(icon, text); + return link; +}; + Glow.openProfile = async username => { Glow.state.activeView = "profile"; Glow.state.activeProfile = null; @@ -103,8 +227,17 @@ Glow.renderProfileView = (editing = false) => { const card = document.createElement("div"); card.className = "profile-card"; - const banner = document.createElement("div"); + const banner = document.createElement(profile.is_self ? "button" : "div"); banner.className = "profile-banner"; + if (profile.is_self) { + banner.type = "button"; + banner.classList.add("profile-banner-button"); + banner.title = "Choose banner image"; + banner.addEventListener("click", () => { + Glow.profileOpenMediaPicker("banner"); + }); + } + if (profile.profile_banner_color) { banner.style.background = `linear-gradient(135deg, ${profile.profile_banner_color}, transparent 72%), ${profile.profile_banner_color}`; @@ -116,18 +249,34 @@ Glow.renderProfileView = (editing = false) => { banner.style.backgroundPosition = "center"; } + if (profile.is_self) { + const bannerHint = document.createElement("span"); + bannerHint.className = "profile-banner-hint"; + bannerHint.textContent = "Change banner"; + banner.append(bannerHint); + } + const content = document.createElement("div"); content.className = "profile-content"; const header = document.createElement("div"); header.className = "profile-header"; - const avatar = document.createElement("div"); + const avatar = document.createElement(profile.is_self ? "button" : "div"); avatar.className = "profile-avatar"; avatar.textContent = (profile.display_name || profile.username || "?") .slice(0, 1) .toUpperCase(); + if (profile.is_self) { + avatar.type = "button"; + avatar.classList.add("profile-avatar-button"); + avatar.title = "Choose avatar image"; + avatar.addEventListener("click", () => { + Glow.profileOpenMediaPicker("avatar"); + }); + } + const actions = document.createElement("div"); actions.className = "profile-actions"; if (profile.is_self) { @@ -170,18 +319,22 @@ Glow.renderProfileView = (editing = false) => { badges.className = "profile-badges"; const roleBadge = document.createElement("span"); - roleBadge.className = "profile-badge"; + roleBadge.className = `profile-badge role role-${String(profile.role || "user").toLowerCase()}`; roleBadge.textContent = profile.role; const statusBadge = document.createElement("span"); - statusBadge.className = "profile-badge"; + statusBadge.className = `profile-badge status status-${String(profile.status || "active").toLowerCase()}`; statusBadge.textContent = profile.status; - const presenceBadge = document.createElement("span"); - presenceBadge.className = "profile-badge"; - presenceBadge.textContent = profile.presence_mode || "online"; + badges.append(roleBadge, statusBadge); - badges.append(roleBadge, statusBadge, presenceBadge); + if (Glow.profileCanShowPresenceBadge(profile)) { + const presenceBadge = document.createElement("span"); + presenceBadge.className = + `profile-badge presence presence-${String(profile.presence_mode || "online").toLowerCase()}`; + presenceBadge.textContent = profile.presence_mode || "online"; + badges.append(presenceBadge); + } const grid = document.createElement("div"); grid.className = "profile-grid"; @@ -203,30 +356,42 @@ Glow.renderProfileView = (editing = false) => { metaList.className = "profile-meta-list"; const addMeta = (label, value) => { + if (!value) return; const dt = document.createElement("dt"); dt.textContent = label; const dd = document.createElement("dd"); - dd.textContent = value || "—"; + 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"); + addMeta("Joined", profile.created_at + ? new Date(profile.created_at).toLocaleDateString() + : ""); + + addMeta("Location", profile.location || ""); + + if (profile.last_active_at) { + addMeta("Last active", new Date(profile.last_active_at).toLocaleString()); + } + + const websiteBadge = Glow.profileBuildWebsiteBadge(profile.website || ""); + if (websiteBadge) { + const dt = document.createElement("dt"); + dt.textContent = "Website"; + const dd = document.createElement("dd"); + dd.append(websiteBadge); + metaList.append(dt, dd); + } + + if (!metaList.children.length) { + const empty = document.createElement("div"); + empty.className = "profile-empty"; + empty.textContent = "No public profile details yet."; + metaSection.append(metaTitle, empty); + } else { + metaSection.append(metaTitle, metaList); + } - metaSection.append(metaTitle, metaList); grid.append(bioSection, metaSection); } @@ -253,6 +418,40 @@ Glow.buildProfileEditor = profile => { const editor = document.createElement("div"); editor.className = "profile-editor"; + const uploadInfo = document.createElement("div"); + uploadInfo.className = "profile-upload-section"; + + const uploadTitle = document.createElement("div"); + uploadTitle.className = "profile-upload-title"; + uploadTitle.textContent = "Profile images"; + + const uploadText = document.createElement("p"); + uploadText.className = "header-subtitle"; + uploadText.textContent = + "Choose new avatar or banner images. A preview/crop step opens before the file is staged."; + + const uploadActions = document.createElement("div"); + uploadActions.className = "settings-actions"; + + const avatarButton = document.createElement("button"); + avatarButton.className = "button secondary"; + avatarButton.type = "button"; + avatarButton.textContent = "Choose avatar"; + avatarButton.addEventListener("click", () => { + Glow.profileOpenMediaPicker("avatar"); + }); + + const bannerButton = document.createElement("button"); + bannerButton.className = "button secondary"; + bannerButton.type = "button"; + bannerButton.textContent = "Choose banner"; + bannerButton.addEventListener("click", () => { + Glow.profileOpenMediaPicker("banner"); + }); + + uploadActions.append(avatarButton, bannerButton); + uploadInfo.append(uploadTitle, uploadText, uploadActions); + const displayName = document.createElement("input"); displayName.className = "field"; displayName.type = "text"; @@ -281,11 +480,6 @@ 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"; @@ -321,13 +515,13 @@ Glow.buildProfileEditor = profile => { const save = document.createElement("button"); save.className = "button"; save.type = "button"; - save.textContent = "Save profile"; + save.textContent = "Save profile changes"; 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); + const bannerFile = Glow.state.profileMediaDraft.bannerFile; + if (bannerFile) { + const uploaded = await Glow.uploadBannerForProfile(bannerFile); bannerAttachmentId = uploaded?.id || bannerAttachmentId; } @@ -341,6 +535,9 @@ Glow.buildProfileEditor = profile => { banner_attachment_id: bannerAttachmentId }); + Glow.state.profileMediaDraft.avatarFile = null; + Glow.state.profileMediaDraft.bannerFile = null; + Glow.state.activeProfile = { ...data.profile, name_color: Glow.state.settings?.name_color || profile.name_color || null, @@ -354,16 +551,16 @@ Glow.buildProfileEditor = profile => { } }); - const preview = document.createElement("button"); - preview.className = "button secondary"; - preview.type = "button"; - preview.textContent = "Preview"; - preview.addEventListener("click", async () => { + const apply = document.createElement("button"); + apply.className = "button secondary"; + apply.type = "button"; + apply.textContent = "Apply changes"; + apply.addEventListener("click", async () => { try { let bannerAttachmentId = profile.banner_attachment_id || null; - const file = bannerInput.files?.[0]; - if (file) { - const uploaded = await Glow.uploadBannerForProfile(file); + const bannerFile = Glow.state.profileMediaDraft.bannerFile; + if (bannerFile) { + const uploaded = await Glow.uploadBannerForProfile(bannerFile); bannerAttachmentId = uploaded?.id || bannerAttachmentId; } @@ -385,17 +582,17 @@ Glow.buildProfileEditor = profile => { }; Glow.renderProfileView(false); } catch (error) { - Glow.showToast(error.message || "Could not preview profile.", "error"); + Glow.showToast(error.message || "Could not apply profile changes.", "error"); } }); - actions.append(save, preview); + actions.append(save, apply); editor.append( + uploadInfo, displayName, statusMessage, website, location, - bannerInput, modeRow, help, body, @@ -403,4 +600,34 @@ Glow.buildProfileEditor = profile => { ); section.append(title, editor); return section; -}; \ No newline at end of file +}; + +Glow.els.avatarUploadInput?.addEventListener("change", () => { + const file = Glow.els.avatarUploadInput.files?.[0]; + if (!file) return; + Glow.profileOpenCropDialog("avatar", file); + Glow.els.avatarUploadInput.value = ""; +}); + +Glow.els.bannerUploadInput?.addEventListener("change", () => { + const file = Glow.els.bannerUploadInput.files?.[0]; + if (!file) return; + Glow.profileOpenCropDialog("banner", file); + Glow.els.bannerUploadInput.value = ""; +}); + +Glow.els.imageCropCancel?.addEventListener("click", () => { + Glow.profileResetCropState(); +}); + +Glow.els.imageCropApply?.addEventListener("click", () => { + Glow.profileApplyCropSelection(); +}); + +Glow.els.imageCropZoom?.addEventListener("input", () => { + Glow.state.imageCrop.zoom = Number(Glow.els.imageCropZoom.value || "1") || 1; +}); + +Glow.els.imageCropAuto?.addEventListener("change", () => { + Glow.state.imageCrop.autoCrop = Boolean(Glow.els.imageCropAuto.checked); +}); \ No newline at end of file