Update static/js/09-profile.js
This commit is contained in:
parent
e65ccce19c
commit
c04b19602e
@ -1,11 +1,6 @@
|
|||||||
// static/js/09-profile.js
|
// static/js/09-profile.js
|
||||||
"use strict";
|
"use strict";
|
||||||
window.Glow = window.Glow || {};
|
window.Glow = window.Glow || {};
|
||||||
const showPresenceModeMeta = false;
|
|
||||||
|
|
||||||
if (showPresenceModeMeta) {
|
|
||||||
addMeta("Presence mode", profile.presence_mode || "online");
|
|
||||||
}
|
|
||||||
|
|
||||||
Glow.escapeHtml = value =>
|
Glow.escapeHtml = value =>
|
||||||
String(value ?? "")
|
String(value ?? "")
|
||||||
@ -28,6 +23,135 @@ Glow.renderProfileBody = profile => {
|
|||||||
return wrap;
|
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.openProfile = async username => {
|
||||||
Glow.state.activeView = "profile";
|
Glow.state.activeView = "profile";
|
||||||
Glow.state.activeProfile = null;
|
Glow.state.activeProfile = null;
|
||||||
@ -103,8 +227,17 @@ Glow.renderProfileView = (editing = false) => {
|
|||||||
const card = document.createElement("div");
|
const card = document.createElement("div");
|
||||||
card.className = "profile-card";
|
card.className = "profile-card";
|
||||||
|
|
||||||
const banner = document.createElement("div");
|
const banner = document.createElement(profile.is_self ? "button" : "div");
|
||||||
banner.className = "profile-banner";
|
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) {
|
if (profile.profile_banner_color) {
|
||||||
banner.style.background =
|
banner.style.background =
|
||||||
`linear-gradient(135deg, ${profile.profile_banner_color}, transparent 72%), ${profile.profile_banner_color}`;
|
`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";
|
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");
|
const content = document.createElement("div");
|
||||||
content.className = "profile-content";
|
content.className = "profile-content";
|
||||||
|
|
||||||
const header = document.createElement("div");
|
const header = document.createElement("div");
|
||||||
header.className = "profile-header";
|
header.className = "profile-header";
|
||||||
|
|
||||||
const avatar = document.createElement("div");
|
const avatar = document.createElement(profile.is_self ? "button" : "div");
|
||||||
avatar.className = "profile-avatar";
|
avatar.className = "profile-avatar";
|
||||||
avatar.textContent = (profile.display_name || profile.username || "?")
|
avatar.textContent = (profile.display_name || profile.username || "?")
|
||||||
.slice(0, 1)
|
.slice(0, 1)
|
||||||
.toUpperCase();
|
.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");
|
const actions = document.createElement("div");
|
||||||
actions.className = "profile-actions";
|
actions.className = "profile-actions";
|
||||||
if (profile.is_self) {
|
if (profile.is_self) {
|
||||||
@ -170,18 +319,22 @@ Glow.renderProfileView = (editing = false) => {
|
|||||||
badges.className = "profile-badges";
|
badges.className = "profile-badges";
|
||||||
|
|
||||||
const roleBadge = document.createElement("span");
|
const roleBadge = document.createElement("span");
|
||||||
roleBadge.className = "profile-badge";
|
roleBadge.className = `profile-badge role role-${String(profile.role || "user").toLowerCase()}`;
|
||||||
roleBadge.textContent = profile.role;
|
roleBadge.textContent = profile.role;
|
||||||
|
|
||||||
const statusBadge = document.createElement("span");
|
const statusBadge = document.createElement("span");
|
||||||
statusBadge.className = "profile-badge";
|
statusBadge.className = `profile-badge status status-${String(profile.status || "active").toLowerCase()}`;
|
||||||
statusBadge.textContent = profile.status;
|
statusBadge.textContent = profile.status;
|
||||||
|
|
||||||
const presenceBadge = document.createElement("span");
|
badges.append(roleBadge, statusBadge);
|
||||||
presenceBadge.className = "profile-badge";
|
|
||||||
presenceBadge.textContent = profile.presence_mode || "online";
|
|
||||||
|
|
||||||
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");
|
const grid = document.createElement("div");
|
||||||
grid.className = "profile-grid";
|
grid.className = "profile-grid";
|
||||||
@ -203,30 +356,42 @@ Glow.renderProfileView = (editing = false) => {
|
|||||||
metaList.className = "profile-meta-list";
|
metaList.className = "profile-meta-list";
|
||||||
|
|
||||||
const addMeta = (label, value) => {
|
const addMeta = (label, value) => {
|
||||||
|
if (!value) return;
|
||||||
const dt = document.createElement("dt");
|
const dt = document.createElement("dt");
|
||||||
dt.textContent = label;
|
dt.textContent = label;
|
||||||
const dd = document.createElement("dd");
|
const dd = document.createElement("dd");
|
||||||
dd.textContent = value || "—";
|
dd.textContent = value;
|
||||||
metaList.append(dt, dd);
|
metaList.append(dt, dd);
|
||||||
};
|
};
|
||||||
|
|
||||||
addMeta("Display name", profile.display_name || profile.username);
|
addMeta("Joined", profile.created_at
|
||||||
addMeta("Status message", profile.status_message || "");
|
? new Date(profile.created_at).toLocaleDateString()
|
||||||
addMeta("Presence mode", profile.presence_mode || "online");
|
: "");
|
||||||
addMeta("Website", profile.website || "");
|
|
||||||
addMeta("Location", profile.location || "");
|
addMeta("Location", profile.location || "");
|
||||||
addMeta(
|
|
||||||
"Joined",
|
if (profile.last_active_at) {
|
||||||
profile.created_at ? new Date(profile.created_at).toLocaleDateString() : ""
|
addMeta("Last active", new Date(profile.last_active_at).toLocaleString());
|
||||||
);
|
}
|
||||||
addMeta(
|
|
||||||
"Last active",
|
const websiteBadge = Glow.profileBuildWebsiteBadge(profile.website || "");
|
||||||
profile.last_active_at ? new Date(profile.last_active_at).toLocaleString() : ""
|
if (websiteBadge) {
|
||||||
);
|
const dt = document.createElement("dt");
|
||||||
addMeta("Bio mode", profile.bio_mode || "txt");
|
dt.textContent = "Website";
|
||||||
addMeta("Name color", profile.name_color || "role default");
|
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);
|
grid.append(bioSection, metaSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +418,40 @@ Glow.buildProfileEditor = profile => {
|
|||||||
const editor = document.createElement("div");
|
const editor = document.createElement("div");
|
||||||
editor.className = "profile-editor";
|
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");
|
const displayName = document.createElement("input");
|
||||||
displayName.className = "field";
|
displayName.className = "field";
|
||||||
displayName.type = "text";
|
displayName.type = "text";
|
||||||
@ -281,11 +480,6 @@ Glow.buildProfileEditor = profile => {
|
|||||||
location.placeholder = "Location";
|
location.placeholder = "Location";
|
||||||
location.value = profile.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");
|
const modeRow = document.createElement("div");
|
||||||
modeRow.className = "profile-mode-row";
|
modeRow.className = "profile-mode-row";
|
||||||
let selectedMode = profile.bio_mode || "txt";
|
let selectedMode = profile.bio_mode || "txt";
|
||||||
@ -321,13 +515,13 @@ Glow.buildProfileEditor = profile => {
|
|||||||
const save = document.createElement("button");
|
const save = document.createElement("button");
|
||||||
save.className = "button";
|
save.className = "button";
|
||||||
save.type = "button";
|
save.type = "button";
|
||||||
save.textContent = "Save profile";
|
save.textContent = "Save profile changes";
|
||||||
save.addEventListener("click", async () => {
|
save.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
let bannerAttachmentId = profile.banner_attachment_id || null;
|
let bannerAttachmentId = profile.banner_attachment_id || null;
|
||||||
const file = bannerInput.files?.[0];
|
const bannerFile = Glow.state.profileMediaDraft.bannerFile;
|
||||||
if (file) {
|
if (bannerFile) {
|
||||||
const uploaded = await Glow.uploadBannerForProfile(file);
|
const uploaded = await Glow.uploadBannerForProfile(bannerFile);
|
||||||
bannerAttachmentId = uploaded?.id || bannerAttachmentId;
|
bannerAttachmentId = uploaded?.id || bannerAttachmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -341,6 +535,9 @@ Glow.buildProfileEditor = profile => {
|
|||||||
banner_attachment_id: bannerAttachmentId
|
banner_attachment_id: bannerAttachmentId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Glow.state.profileMediaDraft.avatarFile = null;
|
||||||
|
Glow.state.profileMediaDraft.bannerFile = null;
|
||||||
|
|
||||||
Glow.state.activeProfile = {
|
Glow.state.activeProfile = {
|
||||||
...data.profile,
|
...data.profile,
|
||||||
name_color: Glow.state.settings?.name_color || profile.name_color || null,
|
name_color: Glow.state.settings?.name_color || profile.name_color || null,
|
||||||
@ -354,16 +551,16 @@ Glow.buildProfileEditor = profile => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const preview = document.createElement("button");
|
const apply = document.createElement("button");
|
||||||
preview.className = "button secondary";
|
apply.className = "button secondary";
|
||||||
preview.type = "button";
|
apply.type = "button";
|
||||||
preview.textContent = "Preview";
|
apply.textContent = "Apply changes";
|
||||||
preview.addEventListener("click", async () => {
|
apply.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
let bannerAttachmentId = profile.banner_attachment_id || null;
|
let bannerAttachmentId = profile.banner_attachment_id || null;
|
||||||
const file = bannerInput.files?.[0];
|
const bannerFile = Glow.state.profileMediaDraft.bannerFile;
|
||||||
if (file) {
|
if (bannerFile) {
|
||||||
const uploaded = await Glow.uploadBannerForProfile(file);
|
const uploaded = await Glow.uploadBannerForProfile(bannerFile);
|
||||||
bannerAttachmentId = uploaded?.id || bannerAttachmentId;
|
bannerAttachmentId = uploaded?.id || bannerAttachmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,17 +582,17 @@ Glow.buildProfileEditor = profile => {
|
|||||||
};
|
};
|
||||||
Glow.renderProfileView(false);
|
Glow.renderProfileView(false);
|
||||||
} catch (error) {
|
} 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(
|
editor.append(
|
||||||
|
uploadInfo,
|
||||||
displayName,
|
displayName,
|
||||||
statusMessage,
|
statusMessage,
|
||||||
website,
|
website,
|
||||||
location,
|
location,
|
||||||
bannerInput,
|
|
||||||
modeRow,
|
modeRow,
|
||||||
help,
|
help,
|
||||||
body,
|
body,
|
||||||
@ -403,4 +600,34 @@ Glow.buildProfileEditor = profile => {
|
|||||||
);
|
);
|
||||||
section.append(title, editor);
|
section.append(title, editor);
|
||||||
return section;
|
return section;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user