feature: profile settings moved to the sidebar
This commit is contained in:
parent
ce5cd81b10
commit
b080e7ab8c
4 changed files with 173 additions and 291 deletions
|
|
@ -96,8 +96,8 @@ select {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
form input,
|
||||||
textarea {
|
form textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,13 @@
|
||||||
window.addEventListener("locationchange", update_topics);
|
window.addEventListener("locationchange", update_topics);
|
||||||
document.addEventListener( 'user_logged_in', update_topics );
|
document.addEventListener( 'user_logged_in', update_topics );
|
||||||
|
|
||||||
|
document.addEventListener( 'user_logged_in', () => {
|
||||||
|
const user = document.body.dataset.user && JSON.parse( document.body.dataset.user );
|
||||||
|
if ( !user ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/* check if we are logged in */
|
/* check if we are logged in */
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -158,44 +158,49 @@
|
||||||
|
|
||||||
new MutationObserver((mutations, observer) => {
|
new MutationObserver((mutations, observer) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
const user_json = document.body.dataset.user;
|
const user = document.body.dataset.user
|
||||||
const user = user_json
|
? JSON.parse(document.body.dataset.user)
|
||||||
? JSON.parse(user_json)
|
: null;
|
||||||
: {
|
|
||||||
username: "",
|
|
||||||
meta: {
|
|
||||||
avatar: DEFAULT_AVATAR_URL,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ids = document.querySelectorAll("[data-bind__user_id]");
|
const user_bound_elements = document.querySelectorAll("[data-bind-to-user-field]");
|
||||||
for (const id of ids) {
|
for (const user_bound_element of user_bound_elements) {
|
||||||
const bound_to =
|
const key =
|
||||||
typeof id.dataset["bind__user_id"] === "string" &&
|
user_bound_element.dataset
|
||||||
id.dataset["bind__user_id"].length > 0
|
.bindToUserField; /* I hate that it converts the name */
|
||||||
? id.dataset["bind__user_id"]
|
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";
|
: "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]");
|
const primary_color_setting = user.meta.primary_color;
|
||||||
for (const avatar of avatars) {
|
if (primary_color_setting) {
|
||||||
const bound_to =
|
const root = document.querySelector(":root");
|
||||||
typeof avatar.dataset["bind__user_meta_avatar"] === "string" &&
|
root.style.setProperty("--base-color", primary_color_setting);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).observe(document.body, {
|
}).observe(document.body, {
|
||||||
|
|
@ -233,53 +238,74 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="profile-container">
|
<form class="profile-container">
|
||||||
<div class="avatar-container">
|
<script>
|
||||||
<img
|
const profile_form = document.currentScript.closest("form");
|
||||||
id="user-avatar"
|
|
||||||
src="/images/default_avatar.gif"
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
alt="User Avatar"
|
const inputs = profile_form.querySelectorAll("input");
|
||||||
data-bind__user_meta_avatar="src"
|
|
||||||
/>
|
async function update_from_input(input) {
|
||||||
<input type="file" accept="image/*" name="avatar" />
|
delete input.__debounce_timeout;
|
||||||
<script>
|
|
||||||
const avatar_file_input = document.querySelector('input[name="avatar"]');
|
if (!document.body.dataset.user) {
|
||||||
avatar_file_input.addEventListener("change", async (event) => {
|
return;
|
||||||
const user_json = document.body.dataset.user;
|
|
||||||
if (!user_json) {
|
|
||||||
return alert("You must be logged in.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = JSON.parse(user_json);
|
const user = JSON.parse(document.body.dataset.user);
|
||||||
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 updated_user = { ...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}`, {
|
const saved_user_response = await api.fetch(`/api/users/${user.id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
@ -295,12 +321,38 @@
|
||||||
|
|
||||||
document.body.dataset.user = JSON.stringify(saved_user);
|
document.body.dataset.user = JSON.stringify(saved_user);
|
||||||
document.body.dataset.perms = saved_user.permissions.join(":");
|
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>
|
||||||
|
|
||||||
<div class="username-container">
|
<div class="username-container">
|
||||||
<span class="username" data-bind__user_username></span>
|
<span class="username" data-bind-to-user-field="username"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="notifications-settings-container">
|
<div class="notifications-settings-container">
|
||||||
|
|
@ -309,27 +361,40 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form data-smart="true" data-method="DELETE" action="/api/auth">
|
<div class="color-settings-container">
|
||||||
<script>
|
<input
|
||||||
{
|
type="text"
|
||||||
const form = document.currentScript.closest("form");
|
id="user-color-setting-primary"
|
||||||
form.on_reply = (response) => {
|
name="meta.primary_color"
|
||||||
if (!response.deleted) {
|
value=""
|
||||||
alert("error logging out? please reload.");
|
data-bind-to-user-field="meta.primary_color"
|
||||||
return;
|
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;
|
<form data-smart="true" data-method="DELETE" action="/api/auth">
|
||||||
delete document.body.dataset.perms;
|
<script>
|
||||||
window.location = "/";
|
{
|
||||||
|
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: {} }));
|
delete document.body.dataset.user;
|
||||||
};
|
delete document.body.dataset.perms;
|
||||||
}
|
window.location = "/";
|
||||||
</script>
|
|
||||||
<button class="primary">Log Out</button>
|
document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
|
||||||
</form>
|
};
|
||||||
</div>
|
}
|
||||||
|
</script>
|
||||||
|
<button class="primary">Log Out</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<div class="topics-container">
|
<div class="topics-container">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,190 +0,0 @@
|
||||||
<script>
|
|
||||||
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
|
|
||||||
|
|
||||||
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 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]");
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).observe(document.body, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ["data-user"],
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
.profile-container {
|
|
||||||
margin: 1rem auto;
|
|
||||||
max-width: 1024px;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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
|
|
||||||
</style>
|
|
||||||
<div id="user" class="tab">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="top-level-tabs"
|
|
||||||
id="user-tab-input"
|
|
||||||
class="tab-switch"
|
|
||||||
data-view="profile"
|
|
||||||
/>
|
|
||||||
<label for="user-tab-input" class="tab-label"
|
|
||||||
><div class="icon user"></div>
|
|
||||||
<div class="label">Profile</div></label
|
|
||||||
>
|
|
||||||
<div class="tab-content">
|
|
||||||
<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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="username-container">
|
|
||||||
<span class="username" data-bind__user_username></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="notifications-settings-container">
|
|
||||||
<button onclick="NOTIFICATIONS.request_permission()">Enable Notifications</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete document.body.dataset.user;
|
|
||||||
delete document.body.dataset.perms;
|
|
||||||
window.location = "/";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<button class="primary">Log Out</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue