autonomous.contact/public/sidebar/sidebar.html

462 lines
12 KiB
HTML

<script>
document.addEventListener("topics_updated", ({ detail: { topics } }) => {
const topic_list = document.getElementById("topic-list");
topic_list.innerHTML = "";
for (const topic of topics) {
topic_list.insertAdjacentHTML(
"beforeend",
`<li id="topic-selector-${topic.id}" class="topic" data-topic-selector-for="${topic.id}"><a href="#/topic/${topic.id}/chat">${topic.name}</a></li>`,
);
}
});
function update_topic_indicators() {
const topic_indicators = document.querySelectorAll("[data-topic-selector-for]");
for (const topic_indicator of topic_indicators) {
topic_indicator.classList.remove("active");
}
const topic_id = document.body.dataset.topic;
if (!topic_id) {
return;
}
const active_topic_indicators = document.querySelectorAll(
`[data-topic-selector-for="${topic_id}"]`,
);
for (const active_indicator of active_topic_indicators) {
active_indicator.classList.add("active");
}
}
document.addEventListener("topics_updated", update_topic_indicators);
document.addEventListener("topic_changed", update_topic_indicators);
document.addEventListener("user_logged_in", update_topic_indicators);
</script>
<style type="text/css">
main {
position: relative;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: auto 1fr;
}
@media screen and (max-width: 1200px) {
main {
grid-template-columns: auto;
}
}
#sidebar {
z-index: 100;
background: var(--bg);
position: relative;
width: auto;
left: 0;
max-width: 32rem;
padding: 0.5rem;
transition: all ease-in-out 0.33s;
border-right: 1px solid var(--border-subtle);
}
#sidebar #sidebar-toggle,
#sidebar #sidebar-toggle-icon {
opacity: 0;
display: none;
}
@media screen and (max-width: 1200px) {
#sidebar {
position: absolute;
top: 0;
bottom: 0;
transform: translateX(-100%);
}
#sidebar #sidebar-toggle-icon {
opacity: 1;
display: block;
position: absolute;
top: 0.9rem;
right: -2.5rem;
cursor: pointer;
transition: all ease-in-out 0.33s;
background: rgba(128, 128, 128, 0.5);
border-radius: 0 1rem 1rem 0;
padding: 0.5rem;
}
#sidebar .icon {
transition: all ease-in-out 0.15s;
}
#sidebar:has(#sidebar-toggle:checked) {
transform: translateX(0);
}
#sidebar:has(#sidebar-toggle:checked) #sidebar-toggle-icon {
right: 0;
rotate: 180deg;
}
}
#sidebar .title {
text-transform: uppercase;
font-size: small;
font-weight: bold;
line-height: 2rem;
}
#sidebar #topic-creation-container {
margin-top: 0.5rem;
}
#sidebar #topic-creation-container #toggle-topic-creation-form-button {
transform: scale(0.8);
}
#sidebar .topic-list {
list-style-type: none;
}
#sidebar .topic-list > li.topic a:before {
position: absolute;
left: -1.75rem;
top: 0;
font-weight: bold;
font-size: x-large;
content: "#";
color: var(--text);
}
#sidebar .topic-list > li.topic a {
position: relative;
display: block;
width: 100%;
min-height: 1.5rem;
line-height: 1.5rem;
font-weight: bold;
font-size: large;
margin-left: 1.75rem;
text-decoration: none;
}
#sidebar .topic-list > li.topic.active a {
color: var(--accent);
}
</style>
<div id="sidebar">
<input type="checkbox" id="sidebar-toggle" />
<label id="sidebar-toggle-icon" for="sidebar-toggle">
<div class="icon right"></div>
</label>
<script>
const DEFAULT_AVATAR_URL = `//${window.location.host}/images/default_avatar.gif`;
new MutationObserver((mutations, observer) => {
mutations.forEach((mutation) => {
const user = document.body.dataset.user
? JSON.parse(document.body.dataset.user)
: null;
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";
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 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, {
attributes: true,
attributeFilter: ["data-user"],
});
</script>
<style type="text/css">
.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;
}
</style>
<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(document.body.dataset.user);
const updated_user = { ...user };
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",
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(":");
}
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-to-user-field="username"></span>
</div>
<div class="notifications-settings-container">
<button class="mockup" onclick="NOTIFICATIONS.request_permission()">
Enable Notifications
</button>
</div>
<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>
<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 = "/";
document.dispatchEvent(new CustomEvent("user_logged_out", { detail: {} }));
};
}
</script>
<button class="primary">Log Out</button>
</form>
<div class="topics-container">
<div>
<span class="title">topics</span>
</div>
<ul id="topic-list" class="topic-list"></ul>
<div id="topic-creation-container" data-requires-permission="topics.create">
<button
id="toggle-topic-creation-form-button"
onclick="((event) => {
event.preventDefault();
const topic_create_form = document.getElementById( 'topic-create' );
topic_create_form.style[ 'height' ] = topic_create_form.style[ 'height' ] === '5rem' ? '0' : '5rem';
})(event)"
>
<div class="icon plus"></div>
</button>
<form
id="topic-create"
data-smart="true"
action="/api/topics"
method="POST"
style="
margin-top: 1rem;
width: 100%;
overflow: hidden;
height: 0;
overflow: hidden;
transition: all 0.5s;
"
>
<input
id="new-topic-name-input"
type="text"
name="name"
value=""
placeholder="new topic"
/>
<input type="submit" hidden />
<script>
{
const form = document.currentScript.closest("form");
const topic_create_form = document.getElementById("topic-create");
const new_topic_name_input =
document.getElementById("new-topic-name-input");
form.on_reply = (new_topic) => {
const topic_list = document.getElementById("topic-list");
topic_list.insertAdjacentHTML(
"beforeend",
`<li id="topic-selector-${new_topic.id}" class="topic"><a href="#/topic/${new_topic.id}">${new_topic.name}</a></li>`,
);
new_topic_name_input.value = "";
window.location.hash = `/topic/${new_topic.id}`;
topic_create_form.style["height"] = "0";
};
}
</script>
</form>
</div>
</div>
</div>