2025-07-01 15:37:35 -07:00
|
|
|
<script>
|
2025-08-12 16:00:36 -07:00
|
|
|
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
|
|
|
|
2025-07-01 15:37:35 -07:00
|
|
|
new MutationObserver((mutations, observer) => {
|
|
|
|
mutations.forEach((mutation) => {
|
|
|
|
const user_json = document.body.dataset.user;
|
|
|
|
const user = user_json
|
|
|
|
? JSON.parse(user_json)
|
|
|
|
: {
|
|
|
|
username: "",
|
|
|
|
meta: {
|
2025-08-12 16:00:36 -07:00
|
|
|
avatar: DEFAULT_AVATAR_URL,
|
2025-07-01 15:37:35 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2025-08-12 16:00:36 -07:00
|
|
|
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"]
|
|
|
|
: "innerHTML";
|
|
|
|
avatar[bound_to] = user.id ?? "<unknown>";
|
|
|
|
}
|
|
|
|
|
|
|
|
const avatars = document.querySelectorAll("[data-bind__user_meta_avatar]");
|
2025-07-01 15:37:35 -07:00
|
|
|
for (const avatar of avatars) {
|
2025-08-12 16:00:36 -07:00
|
|
|
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;
|
2025-07-01 15:37:35 -07:00
|
|
|
}
|
|
|
|
|
2025-08-12 16:00:36 -07:00
|
|
|
const usernames = document.querySelectorAll("[data-bind__user_username]");
|
2025-07-01 15:37:35 -07:00
|
|
|
for (const username of usernames) {
|
2025-08-12 16:00:36 -07:00
|
|
|
const bound_to =
|
|
|
|
typeof username.dataset["bind__user_username"] === "string" &&
|
|
|
|
username.dataset["bind__user_username"].length > 0
|
|
|
|
? username.dataset["bind__user_username"]
|
|
|
|
: "innerHTML";
|
2025-07-01 15:37:35 -07:00
|
|
|
username[bound_to] = user.username;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}).observe(document.body, {
|
|
|
|
attributes: true,
|
|
|
|
attributeFilter: ["data-user"],
|
|
|
|
});
|
|
|
|
</script>
|
2025-07-04 15:16:51 -07:00
|
|
|
<style>
|
|
|
|
.profile-container {
|
|
|
|
margin: 1rem auto;
|
|
|
|
max-width: 1024px;
|
2025-08-12 16:00:36 -07:00
|
|
|
padding: 1rem;
|
2025-07-04 15:16:51 -07:00
|
|
|
}
|
2025-08-12 16:00:36 -07:00
|
|
|
|
|
|
|
.profile-container .avatar-container {
|
|
|
|
position: relative;
|
|
|
|
width: 100%;
|
|
|
|
max-width: 200px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.profile-container .avatar-container #user-avatar {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
max-width: 100%;
|
|
|
|
max-height: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.profile-container .avatar-container input[type="file"] {
|
|
|
|
position: absolute;
|
|
|
|
top: 0;
|
|
|
|
right: 0;
|
|
|
|
bottom: 0;
|
|
|
|
left: 0;
|
|
|
|
opacity: 0;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
|
|
|
|
.profile-container .avatar-container
|
2025-07-04 15:16:51 -07:00
|
|
|
</style>
|
2025-07-01 15:37:35 -07:00
|
|
|
<div id="user" class="tab">
|
|
|
|
<input
|
|
|
|
type="radio"
|
|
|
|
name="top-level-tabs"
|
|
|
|
id="user-tab-input"
|
|
|
|
class="tab-switch"
|
|
|
|
data-hash="/profile"
|
|
|
|
/>
|
|
|
|
<label for="user-tab-input" class="tab-label"
|
|
|
|
><div class="icon user"></div>
|
|
|
|
<div class="label">Profile</div></label
|
|
|
|
>
|
|
|
|
<div class="tab-content">
|
2025-07-04 15:16:51 -07:00
|
|
|
<div class="profile-container">
|
|
|
|
<div class="avatar-container">
|
|
|
|
<img
|
|
|
|
id="user-avatar"
|
|
|
|
src="/images/default_avatar.gif"
|
|
|
|
alt="User Avatar"
|
2025-08-12 16:00:36 -07:00
|
|
|
data-bind__user_meta_avatar="src"
|
2025-07-04 15:16:51 -07:00
|
|
|
/>
|
2025-08-12 16:00:36 -07:00
|
|
|
<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.");
|
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
2025-08-20 11:48:16 -07:00
|
|
|
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
|
2025-08-12 16:00:36 -07:00
|
|
|
|
|
|
|
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 updated_user = { ...user };
|
|
|
|
updated_user.meta = updated_user.meta ?? {};
|
|
|
|
updated_user.meta.avatar = `//${window.location.host}${avatar_path}`;
|
|
|
|
|
|
|
|
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
|
|
|
|
method: "PUT",
|
|
|
|
json: updated_user,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!saved_user_response.ok) {
|
|
|
|
const error = await avatar_upload_response.json();
|
|
|
|
return alert(error?.error?.message ?? "Unknown error.");
|
|
|
|
}
|
|
|
|
|
|
|
|
const saved_user = await saved_user_response.json();
|
|
|
|
|
|
|
|
document.body.dataset.user = JSON.stringify(saved_user);
|
|
|
|
document.body.dataset.perms = saved_user.permissions.join(":");
|
|
|
|
});
|
|
|
|
</script>
|
2025-07-04 15:16:51 -07:00
|
|
|
</div>
|
2025-07-01 15:37:35 -07:00
|
|
|
|
2025-07-04 15:16:51 -07:00
|
|
|
<div class="username-container">
|
2025-08-12 16:00:36 -07:00
|
|
|
<span class="username" data-bind__user_username></span>
|
2025-07-04 15:16:51 -07:00
|
|
|
</div>
|
2025-07-04 14:51:49 -07:00
|
|
|
|
2025-07-04 15:16:51 -07:00
|
|
|
<form data-smart="true" data-method="DELETE" action="/api/auth">
|
|
|
|
<script>
|
|
|
|
{
|
|
|
|
const form = document.currentScript.closest("form");
|
2025-07-11 18:33:32 -07:00
|
|
|
form.on_reply = (response) => {
|
2025-07-04 15:16:51 -07:00
|
|
|
if (!response.deleted) {
|
|
|
|
alert("error logging out? please reload.");
|
|
|
|
return;
|
|
|
|
}
|
2025-07-04 14:51:49 -07:00
|
|
|
|
2025-07-04 15:16:51 -07:00
|
|
|
delete document.body.dataset.user;
|
|
|
|
delete document.body.dataset.perms;
|
|
|
|
window.location = "/";
|
|
|
|
};
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
<button class="primary">Log Out</button>
|
|
|
|
</form>
|
|
|
|
</div>
|
2025-07-01 15:37:35 -07:00
|
|
|
</div>
|
|
|
|
</div>
|