feature: profile settings moved to the sidebar

This commit is contained in:
Andy Burke 2025-09-15 12:02:03 -07:00
parent ce5cd81b10
commit b080e7ab8c
4 changed files with 173 additions and 291 deletions

View file

@ -158,44 +158,49 @@
new MutationObserver((mutations, observer) => {
mutations.forEach((mutation) => {
const user_json = document.body.dataset.user;
const user = user_json
? JSON.parse(user_json)
: {
username: "",
meta: {
avatar: DEFAULT_AVATAR_URL,
},
};
const user = document.body.dataset.user
? JSON.parse(document.body.dataset.user)
: null;
const ids = document.querySelectorAll("[data-bind__user_id]");
for (const id of ids) {
const bound_to =
typeof id.dataset["bind__user_id"] === "string" &&
id.dataset["bind__user_id"].length > 0
? id.dataset["bind__user_id"]
const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
for (const user_bound_element of user_bound_elements) {
const key =
user_bound_element.dataset
.bindToUserField; /* I hate that it converts the name */
const key_elements = key.split(".");
let value = undefined;
if (user) {
let current = user;
for (const key_element of key_elements) {
current = current[key_element];
if (!current) {
break;
}
}
value = current;
}
const target =
typeof user_bound_element.dataset.userFieldTarget === "string" &&
user_bound_element.dataset.userFieldTarget.length > 0
? user_bound_element.dataset.userFieldTarget
: "innerHTML";
avatar[bound_to] = user.id ?? "<unknown>";
const default_value =
typeof user_bound_element.dataset.userFieldDefault === "string" &&
user_bound_element.dataset.userFieldDefault.length > 0
? user_bound_element.dataset.userFieldDefault
: "<unknown>";
user_bound_element[target] = value ?? default_value;
}
const avatars = document.querySelectorAll("[data-bind__user_meta_avatar]");
for (const avatar of avatars) {
const bound_to =
typeof avatar.dataset["bind__user_meta_avatar"] === "string" &&
avatar.dataset["bind__user_meta_avatar"].length
? avatar.dataset["bind__user_meta_avatar"]
: "innerHTML";
avatar[bound_to] = user.meta?.avatar ?? DEFAULT_AVATAR_URL;
}
const usernames = document.querySelectorAll("[data-bind__user_username]");
for (const username of usernames) {
const bound_to =
typeof username.dataset["bind__user_username"] === "string" &&
username.dataset["bind__user_username"].length > 0
? username.dataset["bind__user_username"]
: "innerHTML";
username[bound_to] = user.username;
const primary_color_setting = user.meta.primary_color;
if (primary_color_setting) {
const root = document.querySelector(":root");
root.style.setProperty("--base-color", primary_color_setting);
}
});
}).observe(document.body, {
@ -233,53 +238,74 @@
cursor: pointer;
}
</style>
<div class="profile-container">
<div class="avatar-container">
<img
id="user-avatar"
src="/images/default_avatar.gif"
alt="User Avatar"
data-bind__user_meta_avatar="src"
/>
<input type="file" accept="image/*" name="avatar" />
<script>
const avatar_file_input = document.querySelector('input[name="avatar"]');
avatar_file_input.addEventListener("change", async (event) => {
const user_json = document.body.dataset.user;
if (!user_json) {
return alert("You must be logged in.");
<form class="profile-container">
<script>
const profile_form = document.currentScript.closest("form");
document.addEventListener("DOMContentLoaded", () => {
const inputs = profile_form.querySelectorAll("input");
async function update_from_input(input) {
delete input.__debounce_timeout;
if (!document.body.dataset.user) {
return;
}
const user = JSON.parse(user_json);
const avatar = avatar_file_input.files[0];
if (!avatar || !avatar.type || !avatar.type.includes("image")) {
return alert("You must select a valid image to upload as your avatar.");
}
// TODO: actually enforce this on the upload in serverus somehow
if (avatar.size > 512_000) {
return alert("512K is the largest allowed avatar size.");
}
const body = new FormData();
body.append("file", avatar, encodeURIComponent(avatar.name));
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
const avatar_upload_response = await api.fetch(avatar_path, {
method: "PUT",
body,
});
if (!avatar_upload_response.ok) {
const error = await avatar_upload_response.json();
return alert(error?.error?.message ?? "Unknown error.");
}
const user = JSON.parse(document.body.dataset.user);
const updated_user = { ...user };
updated_user.meta = updated_user.meta ?? {};
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
switch (input.name) {
case "meta.avatar":
const avatar = input.files[0];
if (!avatar || !avatar.type || !avatar.type.includes("image")) {
return alert(
"You must select a valid image to upload as your avatar.",
);
}
// TODO: actually enforce this on the upload in serverus somehow
if (avatar.size > 512_000) {
return alert("512K is the largest allowed avatar size.");
}
const avatar_upload_body = new FormData();
avatar_upload_body.append(
"file",
avatar,
encodeURIComponent(avatar.name),
);
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
const avatar_upload_response = await api.fetch(avatar_path, {
method: "PUT",
body: avatar_upload_body,
});
if (!avatar_upload_response.ok) {
const error = await avatar_upload_response.json();
return alert(error?.error?.message ?? "Unknown error.");
}
updated_user.meta = updated_user.meta ?? {};
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
break;
default:
const elements = input.name.split(".");
let current = updated_user;
for (const element of elements.slice(0, elements.length - 1)) {
current[element] = current[element] ?? {};
current = current[element];
}
current[elements.slice(elements.length - 1).shift()] =
input.value.trim();
break;
}
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
method: "PUT",
@ -295,12 +321,38 @@
document.body.dataset.user = JSON.stringify(saved_user);
document.body.dataset.perms = saved_user.permissions.join(":");
});
</script>
}
for (const input of inputs) {
function on_updated(event) {
if (input.__debounce_timeout) {
clearTimeout(input.__debounce_timeout);
}
input.__debounce_timeout = setTimeout(() => {
update_from_input(input);
}, 250);
}
input.addEventListener("input", on_updated);
input.addEventListener("change", on_updated);
}
});
</script>
<div class="avatar-container">
<img
id="user-avatar"
src="/images/default_avatar.gif"
alt="User Avatar"
data-bind-to-user-field="meta.avatar"
data-user-field-target="src"
data-user-field-default="/images/default_avatar.gif"
/>
<input type="file" accept="image/*" name="meta.avatar" />
</div>
<div class="username-container">
<span class="username" data-bind__user_username></span>
<span class="username" data-bind-to-user-field="username"></span>
</div>
<div class="notifications-settings-container">
@ -309,27 +361,40 @@
</button>
</div>
<form data-smart="true" data-method="DELETE" action="/api/auth">
<script>
{
const form = document.currentScript.closest("form");
form.on_reply = (response) => {
if (!response.deleted) {
alert("error logging out? please reload.");
return;
}
<div class="color-settings-container">
<input
type="text"
id="user-color-setting-primary"
name="meta.primary_color"
value=""
data-bind-to-user-field="meta.primary_color"
data-user-field-target="value"
data-user-field-default=""
/>
<label for="user-color-setting-primary">Primary Color</label>
</div>
</form>
delete document.body.dataset.user;
delete document.body.dataset.perms;
window.location = "/";
<form data-smart="true" data-method="DELETE" action="/api/auth">
<script>
{
const form = document.currentScript.closest("form");
form.on_reply = (response) => {
if (!response.deleted) {
alert("error logging out? please reload.");
return;
}
document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
};
}
</script>
<button class="primary">Log Out</button>
</form>
</div>
delete document.body.dataset.user;
delete document.body.dataset.perms;
window.location = "/";
document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
};
}
</script>
<button class="primary">Log Out</button>
</form>
<div class="topics-container">
<div>