tetratto/crates/app/src/public/html/communities/settings.lisp

1246 lines
48 KiB
Common Lisp
Raw Normal View History

2025-06-01 12:25:33 -04:00
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Community settings - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
(div
2025-08-04 12:12:04 -04:00
("class" "pillmenu rows w-full")
(div
("class" "row")
(a
("href" "#/general")
("data-tab-button" "general")
("class" "active")
(text "{{ icon \"settings\" }}")
(span
(text "{{ text \"settings:tab.general\" }}")))
(a
("href" "#/images")
("data-tab-button" "images")
(text "{{ icon \"image\" }}")
(span
(text "{{ text \"settings:tab.images\" }}")))
(a
("href" "#/members")
("data-tab-button" "members")
(text "{{ icon \"users-round\" }}")
(span
(text "{{ text \"communities:tab.members\" }}"))))
(div
("class" "row")
(text "{% if can_manage_channels -%}")
(a
("href" "#/channels")
("data-tab-button" "channels")
(text "{{ icon \"rss\" }}")
(span
(text "{{ text \"communities:tab.channels\" }}")))
(text "{%- endif %}")
2025-08-04 12:12:04 -04:00
(a
("href" "#/topics")
("data-tab-button" "topics")
(icon (text "list"))
(span
(str (text "communities:tab.topics"))))
(text "{% if can_manage_emojis -%}")
2025-08-04 12:12:04 -04:00
(a
("href" "#/emojis")
("data-tab-button" "emojis")
(text "{{ icon \"smile\" }}")
(span
(text "{{ text \"communities:tab.emojis\" }}")))
(text "{%- endif %}")))
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "w_full flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("data-tab" "general")
(div
("id" "manage_fields")
2025-08-03 23:24:57 -04:00
("class" "card lowered flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "read_access")
(div
("class" "card small")
(b
(text "Read access")))
(div
("class" "card")
(select
("onchange" "save_access(event, 'read')")
(option
("value" "Everybody")
("selected" "{% if community.read_access == 'Everybody' -%}true{% else %}false{%- endif %}")
(text "Everybody"))
(option
("value" "Joined")
("selected" "{% if community.read_access == 'Joined' -%}true{% else %}false{%- endif %}")
(text "Joined")))))
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "join_access")
(div
("class" "card small")
(b
(text "Join access")))
(div
("class" "card")
(select
("onchange" "save_access(event, 'join')")
(option
("value" "Everybody")
("selected" "{% if community.join_access == 'Everybody' -%}true{% else %}false{%- endif %}")
(text "Everybody"))
(option
("value" "Request")
("selected" "{% if community.join_access == 'Request' -%}true{% else %}false{%- endif %}")
(text "Request"))
(option
("value" "Nobody")
("selected" "{% if community.join_access == 'Nobody' -%}true{% else %}false{%- endif %}")
(text "Nobody")))))
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "write_access")
(div
("class" "card small")
(b
(text "Post permission")))
(div
("class" "card")
(select
("onchange" "save_access(event, 'write')")
(option
("value" "Everybody")
("selected" "{% if community.write_access == 'Everybody' -%}true{% else %}false{%- endif %}")
(text "Everybody"))
(option
("value" "Joined")
("selected" "{% if community.write_access == 'Joined' -%}true{% else %}false{%- endif %}")
(text "Joined"))
(option
("value" "Owner")
("selected" "{% if community.write_access == 'Owner' -%}true{% else %}false{%- endif %}")
(text "Owner only")))))
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "change_title")
(div
("class" "card small")
(b
(text "{{ text \"communities:label.change_title\" }}")))
(form
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("onsubmit" "change_title(event)")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1")
2025-06-01 12:25:33 -04:00
(label
("for" "new_title")
(text "{{ text \"communities:label.new_title\" }}"))
(input
("type" "text")
("name" "new_title")
("id" "new_title")
("placeholder" "new_title")
("required" "")
("minlength" "2")))
(button
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}"))))))
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "danger_zone")
(div
2025-08-03 23:24:57 -04:00
("class" "card small flex gap_1 items_center red")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"skull\" }}")
(b
(text "{{ text \"communities:label.danger_zone\" }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_wrap gap_2")
2025-06-01 12:25:33 -04:00
(button
("class" "red lowered")
2025-06-01 12:25:33 -04:00
("onclick" "delete_community()")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"communities:label.delete_community\" }}")))))
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 flex_wrap")
2025-06-01 12:25:33 -04:00
(button
("onclick" "save_context()")
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}")))
(a
("href" "/community/{{ community.title }}")
("class" "button secondary")
(text "{{ icon \"arrow-left\" }}")
(span
(text "{{ text \"general:action.back\" }}")))))
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered w_full hidden flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("data-tab" "images")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "change_avatar")
(div
("class" "card small")
(b
(text "{{ text \"settings:label.change_avatar\" }}")))
(form
2025-08-03 23:24:57 -04:00
("class" "card flex gap_2 flex_row flex_wrap items_center")
2025-06-01 12:25:33 -04:00
("method" "post")
("enctype" "multipart/form-data")
("onsubmit" "upload_avatar(event)")
(input
("id" "avatar_file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
2025-08-03 23:24:57 -04:00
("class" "w_content"))
2025-06-01 12:25:33 -04:00
(button
(text "{{ icon \"check\" }}"))))
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "change_banner")
(div
("class" "card small")
(b
(text "{{ text \"settings:label.change_banner\" }}")))
(form
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("method" "post")
("enctype" "multipart/form-data")
("onsubmit" "upload_banner(event)")
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 flex_row flex_wrap items_center")
2025-06-01 12:25:33 -04:00
(input
("id" "banner_file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp")
2025-08-03 23:24:57 -04:00
("class" "w_content"))
2025-06-01 12:25:33 -04:00
(button
(text "{{ icon \"check\" }}")))
(span
("class" "fade")
(text "Use an image of 1100x350px for the best results.")))))
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered w_full hidden flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("data-tab" "members")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
(div
("class" "card small")
(b
(text "{{ text \"communities:label.select_member\" }}")))
(form
2025-08-03 23:24:57 -04:00
("class" "card flex_col gap_2")
2025-06-01 12:25:33 -04:00
("onsubmit" "select_user_from_form(event)")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1")
2025-06-01 12:25:33 -04:00
(label
("for" "uid")
(text "{{ text \"communities:label.user_id\" }}"))
(input
("type" "number")
("name" "uid")
("id" "uid")
("placeholder" "user id")
("required" "")
("minlength" "18")))
(button
(text "{{ text \"communities:action.select\" }}")))))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2 w_full")
2025-06-02 22:50:12 -04:00
("id" "membership_info"))
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest w_full")
2025-06-02 22:50:12 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card small flex items_center justify_between gap_2")
2025-06-02 22:50:12 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-06-02 22:50:12 -04:00
(text "{{ icon \"blocks\" }}")
(span
(text "{{ text \"mod_panel:label.permissions_level_builder\" }}")))
(button
("class" "small lowered")
2025-06-02 22:50:12 -04:00
("onclick" "update_user_role(document.getElementById('uid').value, document.getElementById('role').value)")
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}"))))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-02 22:50:12 -04:00
("id" "permission_builder"))))
2025-06-01 12:25:33 -04:00
(text "{% if can_manage_channels -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered w_full hidden flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("data-tab" "channels")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
(div
("class" "card small")
(b
(text "{{ text \"communities:action.create_channel\" }}")))
(form
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("onsubmit" "create_channel_from_form(event)")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1")
2025-06-01 12:25:33 -04:00
(label
("for" "title")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "title")
("id" "title")
("placeholder" "name")
("required" "")
("minlength" "2")
("maxlength" "32")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(text "{% for channel in channels %}")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
(div
("class" "card small")
(b
(text "{{ channel.position }} "))
(text "{{ channel.title }}"))
(div
2025-08-03 23:24:57 -04:00
("class" "card flex gap_2")
2025-06-01 12:25:33 -04:00
(button
("class" "red lowered small")
2025-06-01 12:25:33 -04:00
("onclick" "delete_channel('{{ channel.id }}')")
(text "{{ text \"general:action.delete\" }}"))
(button
("class" "lowered small")
2025-06-01 12:25:33 -04:00
("onclick" "update_channel_position('{{ channel.id }}')")
(text "{{ text \"chats:action.move\" }}"))
(button
("class" "lowered small")
2025-06-01 12:25:33 -04:00
("onclick" "update_channel_title('{{ channel.id }}')")
(text "{{ text \"chats:action.rename\" }}"))))
(text "{% endfor %}"))
(script
(text "globalThis.delete_channel = async (id) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/channels/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.update_channel_position = async (id) => {
await trigger(\"atto::debounce\", [\"channels::move\"]);
const position = Number.parseInt(
await trigger(\"atto::prompt\", [
\"New channel position (number):\",
]),
);
if (!position && position !== 0) {
return alert(\"Must be a number!\");
}
fetch(`/api/v1/channels/${id}/move`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
position,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.update_channel_title = async (id) => {
await trigger(\"atto::debounce\", [\"channels::update_title\"]);
const title = await trigger(\"atto::prompt\", [\"New channel title:\"]);
if (!title) {
return;
}
fetch(`/api/v1/channels/${id}/title`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
async function create_channel_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"channels::create\"]);
fetch(\"/api/v1/channels\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.title.value,
community: \"{{ community.id }}\",
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}"))
(text "{%- endif %} {% if can_manage_emojis -%}")
(div
2025-08-03 23:24:57 -04:00
("class" "card lowered w_full hidden flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("data-tab" "emojis")
(text "{{ components::supporter_ad(body=\"Become a supporter to upload GIF animated emojis!\") }}")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-06-01 12:25:33 -04:00
("ui_ident" "change_banner")
(div
2025-08-03 23:24:57 -04:00
("class" "card small flex items_center gap_2")
2025-06-01 12:25:33 -04:00
(text "{{ icon \"upload\" }}")
(b
(text "{{ text \"communities:label.upload\" }}")))
(form
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-06-01 12:25:33 -04:00
("onsubmit" "upload_emoji(event)")
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1")
2025-06-01 12:25:33 -04:00
(label
("for" "name")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "name")
("id" "name")
("placeholder" "name")
("required" "")
("minlength" "2")
("maxlength" "32")))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_1")
2025-06-01 12:25:33 -04:00
(label
("for" "file")
(text "{{ text \"communities:label.file\" }}"))
(input
("id" "banner_file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp")
2025-08-03 23:24:57 -04:00
("class" "w_full")))
2025-06-01 12:25:33 -04:00
(button
(text "{{ text \"communities:action.create\" }}"))
(span
("class" "fade")
(text "Emojis can be a maximum of 256 KiB, or 512x512px (width x
height)."))))
(text "{% for emoji in emojis %}")
(div
2025-08-03 23:24:57 -04:00
("class" "card secondary flex flex_wrap gap_2 items_center justify_between")
2025-06-01 12:25:33 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 items_center")
2025-06-01 12:25:33 -04:00
(img
("src" "/api/v1/communities/{{ community.id }}/emojis/{{ emoji.name }}")
("alt" "{{ emoji.name }}")
("class" "emoji")
("loading" "lazy"))
(b
(text "{{ emoji.name }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
2025-06-01 12:25:33 -04:00
(button
("class" "lowered small")
2025-06-01 12:25:33 -04:00
("onclick" "rename_emoji('{{ emoji.id }}')")
(text "{{ icon \"pencil\" }}")
(span
(text "{{ text \"chats:action.rename\" }}")))
(button
("class" "lowered small red")
2025-06-01 12:25:33 -04:00
("onclick" "remove_emoji('{{ emoji.id }}')")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"stacks:label.remove\" }}")))))
(text "{% endfor %}"))
(script
(text "globalThis.upload_emoji = (e) => {
e.preventDefault();
e.target.querySelector(\"button\").style.display = \"none\";
fetch(
`/api/v1/communities/{{ community.id }}/emojis/${e.target.name.value}`,
{
method: \"POST\",
body: e.target.file.files[0],
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
e.target.querySelector(\"button\").removeAttribute(\"style\");
});
alert(\"Emoji upload in progress. Please wait!\");
};
globalThis.rename_emoji = async (id) => {
const name = await trigger(\"atto::prompt\", [\"New emoji name:\"]);
if (!name) {
return;
}
fetch(`/api/v1/emojis_id/${id}/name`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
name,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.remove_emoji = async (id) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this? This action is permanent.\",
]))
) {
return;
}
fetch(`/api/v1/emojis_id/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};"))
2025-08-04 12:12:04 -04:00
(text "{%- endif %}")
(text "{% if community.is_forum -%}")
(script ("type" "application/json") ("id" "community_topics") (text "{{ community.topics | json_encode() | safe }}"))
(div
("class" "card lowered w_full hidden flex flex_col gap_2")
("data-tab" "topics")
(div
("class" "card_nest")
(div
("class" "card small")
(b
(str (text "communities:action.create_topic"))))
(form
("class" "card flex flex_col gap_2")
("onsubmit" "create_topic_from_form(event)")
(div
("class" "flex flex_col gap_1")
(label
("for" "title")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "title")
("id" "title")
("placeholder" "name")
("required" "")
("minlength" "2")
("maxlength" "32")))
(div
("class" "flex flex_col gap_1")
(label
("for" "description")
(str (text "communities:label.description")))
(input
("type" "text")
("name" "description")
("id" "description")
("placeholder" "description")
("required" "")
("minlength" "2")
("maxlength" "256")))
(div
("class" "flex flex_col gap_1")
(label
("for" "color")
(str (text "communities:label.color")))
(input
("type" "color")
("name" "color")
("id" "color")
("placeholder" "color")
("required" "")
("style" "width: 8rem")))
(div
("class" "flex flex_col gap_1")
(label
("for" "position")
(str (text "communities:label.position")))
(input
("type" "number")
("name" "position")
("id" "position")
("placeholder" "position")
("required" "")
("value" "0")
("min" "0")
("max" "256")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(text "{% for id, topic in community.topics %}")
(div
("class" "card_nest")
(div
("class" "card small flex justify_between gap_2")
(div
("class" "flex gap_2")
(b
(text "{{ topic.position }} "))
(text "{{ topic.title }}"))
(button
("class" "red lowered small")
("onclick" "delete_topic('{{ id }}')")
(icon (text "trash"))
(str (text "general:action.delete"))))
(div
("class" "card flex flex_col gap_2")
(details
("class" "accordion")
(summary ("class" "flex items_center gap_2") (icon (text "pencil")) (str (text "general:label.edit")))
(form
("class" "inner flex flex_col gap_2")
("style" "background: var(--color-super-raised)")
("onsubmit" "update_topic_from_form(event, '{{ id }}')")
(div
("class" "flex flex_col gap_1")
(label
("for" "title")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "title")
("id" "title")
("placeholder" "name")
("value" "{{ topic.title }}")
("required" "")
("minlength" "2")
("maxlength" "32")))
(div
("class" "flex flex_col gap_1")
(label
("for" "{{ id }}-description")
2025-08-04 12:12:04 -04:00
(str (text "communities:label.description")))
(input
("type" "text")
("name" "description")
("id" "{{ id }}-description")
2025-08-04 12:12:04 -04:00
("placeholder" "description")
("value" "{{ topic.description }}")
("required" "")
("minlength" "2")
("maxlength" "256")))
(div
("class" "flex flex_col gap_1")
(label
("for" "{{ id }}-color")
2025-08-04 12:12:04 -04:00
(str (text "communities:label.color")))
(input
("type" "color")
("name" "color")
("id" "{{ id }}-color")
2025-08-04 12:12:04 -04:00
("placeholder" "color")
("required" "")
("value" "{{ topic.color }}")
("style" "width: 8rem")))
(div
("class" "flex flex_col gap_1")
(label
("for" "{{ id }}-position")
2025-08-04 12:12:04 -04:00
(str (text "communities:label.position")))
(input
("type" "number")
("name" "position")
("id" "{{ id }}-position")
2025-08-04 12:12:04 -04:00
("placeholder" "position")
("required" "")
("value" "{{ topic.position }}")
("min" "0")
("max" "256")))
(div
("class" "flex flex_col gap_1")
(label
("for" "{{ id }}-write_access")
(text "Post permission"))
(select
("name" "write_access")
("id" "{{ id }}-write_access")
(option
("value" "Everybody")
("selected" "{% if topic.write_access == 'Everybody' -%}true{% else %}false{%- endif %}")
(text "Everybody"))
(option
("value" "Joined")
("selected" "{% if topic.write_access == 'Joined' -%}true{% else %}false{%- endif %}")
(text "Joined"))
(option
("value" "Owner")
("selected" "{% if topic.write_access == 'Owner' -%}true{% else %}false{%- endif %}")
(text "Owner only"))))
2025-08-04 12:12:04 -04:00
(button
(icon (text "check"))
(str (text "general:action.save")))))))
(text "{% endfor %}"))
(script
(text "globalThis.delete_topic = async (id) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}/topics/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
async function create_topic_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"topics::create\"]);
fetch(\"/api/v1/communities/{{ community.id }}/topics\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.title.value,
description: e.target.description.value,
color: e.target.color.value,
position: Number.parseInt(e.target.position.value),
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
e.target.reset();
window.location.reload();
}
});
}
async function update_topic_from_form(e, id) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"topics::update\"]);
fetch(`/api/v1/communities/{{ community.id }}/topics/${id}`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.title.value,
description: e.target.description.value,
color: e.target.color.value,
position: Number.parseInt(e.target.position.value),
write_access: e.target.write_access.selectedOptions[0].value,
2025-08-04 12:12:04 -04:00
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}"))
(text "{% else %}")
(div
("class" "card lowered w_full hidden flex flex_col gap_2")
("data-tab" "topics")
(p (text "You can only manage topics for forum communities. You can convert this community into a forum, but you will not be able to go back."))
(p (text "This will permanently change your community. Currently existing posts will no longer be visible on the community."))
(button
("onclick" "convert_to_forum()")
(icon (text "circle-fading-arrow-up"))
(text "Switch to forum")))
(script
(text "globalThis.convert_to_forum = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this? It cannot be undone.\",
]))
) {
return;
}
fetch(\"/api/v1/communities/{{ community.id }}/is_forum\", {
method: \"POST\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}"))
2025-06-02 22:50:12 -04:00
(text "{%- endif %}"))
2025-06-01 12:25:33 -04:00
(script
(text "setTimeout(async () => {
2025-06-01 12:25:33 -04:00
const element = document.getElementById(\"membership_info\");
const ui = await ns(\"ui\");
2025-06-01 12:25:33 -04:00
const uid = new URLSearchParams(window.location.search).get(\"uid\");
if (uid) {
document.getElementById(\"uid\").value = uid;
}
globalThis.update_user_role = async (uid, new_role) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
`/api/v1/communities/{{ community.id }}/memberships/${uid}/role`,
{
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
role: Number.parseInt(new_role),
}),
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.kick_user = async (uid, new_role) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}/memberships/${uid}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.transfer_ownership = async (uid) => {
if (
!(await trigger(\"atto::confirm\", [
2025-06-11 13:14:49 -04:00
\"Are you sure you would like to do this?\\n\\nThis action is PERMANENT!\",
2025-06-01 12:25:33 -04:00
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}/transfer_ownership`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
user: uid,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.select_user_from_form = (e) => {
e.preventDefault();
fetch(
`/api/v1/communities/{{ community.id }}/memberships/${e.target.uid.value}`,
)
.then((res) => res.json())
.then(async (res) => {
2025-06-01 12:25:33 -04:00
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (!res.ok) {
return;
}
// permissions manager
const get_permissions_html = await trigger(
2025-06-01 12:25:33 -04:00
\"ui::generate_permissions_ui\",
[
{
// https://trisuaso.github.io/tetratto/tetratto/model/communities_permissions/struct.CommunityPermission.html
DEFAULT: 1 << 0,
ADMINISTRATOR: 1 << 1,
MEMBER: 1 << 2,
MANAGE_POSTS: 1 << 3,
MANAGE_ROLES: 1 << 4,
BANNED: 1 << 5,
REQUESTED: 1 << 6,
MANAGE_PINS: 1 << 7,
MANAGE_COMMUNITY: 1 << 8,
MANAGE_QUESTIONS: 1 << 9,
MANAGE_CHANNELS: 1 << 10,
MANAGE_MESSAGES: 1 << 11,
MANAGE_EMOJIS: 1 << 12,
},
],
);
// ...
2025-08-03 23:24:57 -04:00
element.innerHTML = `<div class=\"flex gap_2 flex_wrap\" ui_ident=\"actions\">
2025-06-01 12:25:33 -04:00
<a target=\"_blank\" class=\"button\" href=\"/api/v1/auth/user/find/${e.target.uid.value}\">Open user profile</a>
${res.payload.role !== 33 ? `<button class=\"red lowered\" onclick=\"update_user_role('${e.target.uid.value}', 33)\">Ban</button>` : `<button class=\"lowered\" onclick=\"update_user_role('${e.target.uid.value}', 5)\">Unban</button>`}
${res.payload.role !== 65 ? `<button class=\"red lowered\" onclick=\"update_user_role('${e.target.uid.value}', 65)\">Send to review</button>` : `<button class=\"green lowered\" onclick=\"update_user_role('${e.target.uid.value}', 5)\">Accept join request</button>`}
<button class=\"red lowered\" onclick=\"kick_user('${e.target.uid.value}')\">Kick</button>
<button class=\"red lowered\" onclick=\"transfer_ownership('${e.target.uid.value}')\">Transfer ownership</button>
2025-06-01 12:25:33 -04:00
</div>`;
document.getElementById(\"permission_builder\").innerHTML =
get_permissions_html(res.payload.role, \"permission_builder\");
2025-06-01 12:25:33 -04:00
ui.refresh_container(element, [\"actions\", \"permissions\"]);
ui.generate_settings_ui(
element,
[
[
[\"role\", \"Permission level\"],
res.payload.role,
\"input\",
],
],
null,
{
role: (new_role) => {
const [matching, _] =
all_matching_permissions(new_role);
document.getElementById(
\"permissions\",
).innerHTML = get_permissions_html(
rebuild_role(matching),
\"permissions\",
);
return update_user_role(
e.target.uid.value,
new_role,
);
},
},
);
});
};
}, 250);"))
(script
("type" "application/json")
("id" "settings_json")
(text "{{ community.context|json_encode()|safe }}"))
(script
(text "setTimeout(async () => {
const ui = await ns(\"ui\");
2025-06-01 12:25:33 -04:00
const settings = JSON.parse(
document.getElementById(\"settings_json\").innerHTML,
);
globalThis.upload_avatar = (e) => {
e.preventDefault();
e.target.querySelector(\"button\").style.display = \"none\";
fetch(\"/api/v1/communities/{{ community.id }}/upload/avatar\", {
method: \"POST\",
body: e.target.file.files[0],
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
e.target.querySelector(\"button\").removeAttribute(\"style\");
});
alert(\"Avatar upload in progress. Please wait!\");
};
globalThis.upload_banner = (e) => {
e.preventDefault();
e.target.querySelector(\"button\").style.display = \"none\";
fetch(\"/api/v1/communities/{{ community.id }}/upload/banner\", {
method: \"POST\",
body: e.target.file.files[0],
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
e.target.querySelector(\"button\").removeAttribute(\"style\");
});
alert(\"Banner upload in progress. Please wait!\");
};
globalThis.save_context = () => {
fetch(\"/api/v1/communities/{{ community.id }}/context\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
context: settings,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.save_access = (event, mode) => {
const selected = event.target.selectedOptions[0];
fetch(`/api/v1/communities/{{ community.id }}/access/${mode}`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
access: selected.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.change_title = async (e) => {
e.preventDefault();
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(\"/api/v1/communities/{{ community.id }}/title\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.new_title.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.delete_community = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this? This action is permanent.\",
]))
) {
return;
}
fetch(`/api/v1/communities/{{ community.id }}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
ui.refresh_container(document.getElementById(\"manage_fields\"), [
\"read_access\",
\"join_access\",
\"write_access\",
\"change_title\",
\"change_avatar\",
\"change_banner\",
]);
const settings_fields = [
[
[\"display_name\", \"Display title\"],
\"{{ community.context.display_name }}\",
\"input\",
],
[
[\"description\", \"Description\"],
settings.description,
\"textarea\",
],
[
[\"is_nsfw\", \"Mark as NSFW\"],
\"{{ community.context.is_nsfw }}\",
\"checkbox\",
]
];
2025-06-09 18:13:53 -04:00
// {% if not community.is_forge %}
settings_fields.push([
2025-06-01 12:25:33 -04:00
[
\"enable_questions\",
\"Allow users to ask questions in this community\",
2025-06-01 12:25:33 -04:00
],
\"{{ community.context.enable_questions }}\",
\"checkbox\",
]);
settings_fields.push([
[
\"enable_titles\",
\"Allow users to attach a title to their posts\",
],
\"{{ community.context.enable_titles }}\",
\"checkbox\",
]);
settings_fields.push([
[
\"require_titles\",
\"Require users to attach a title to their posts\",
],
\"{{ community.context.require_titles }}\",
\"checkbox\",
]);
2025-06-09 18:13:53 -04:00
// {% endif %}
ui.generate_settings_ui(
document.getElementById(\"manage_fields\"),
settings_fields,
2025-06-01 12:25:33 -04:00
settings,
);
}, 250);"))
(text "{% endblock %}")