add: finish ui rewrite

This commit is contained in:
trisua 2025-06-01 12:25:33 -04:00
parent e9846016e6
commit 5dec98d698
119 changed files with 8776 additions and 9350 deletions

View file

@ -1,340 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>{{ community.context.display_name }} - {{ config.name }}</title>
<meta name="og:title" content="{{ community.title }}" />
<meta
name="description"
content='View the "{{ community.title }}" community on {{ config.name }}!'
/>
<meta
name="og:description"
content='View the "{{ community.title }}" community on {{ config.name }}!'
/>
<meta property="og:type" content="profile" />
<meta property="profile:username" content="{{ community.title }}" />
<meta
name="og:image"
content="{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar"
/>
<meta
name="twitter:image"
content="{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar"
/>
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ community.title }}" />
<meta
name="twitter:description"
content='View the "{{ community.title }}" community on {{ config.name }}!'
/>
{% endblock %} {% block body %} {{ macros::nav() }}
<article>
<div class="content_container flex flex-col gap-4">
{{ components::community_banner(id=community.id, community=community) }}
<div class="w-full flex gap-4 flex-collapse">
<div
class="lhs flex flex-col gap-2 sm:w-full"
style="width: 22rem; min-width: 22rem"
>
<div class="card-nest w-full">
<div class="card flex gap-2" id="community_avatar_and_name">
{{ components::community_avatar(id=community.id,
community=community, size="72px") }}
<div class="flex flex-col">
<div class="flex gap-2 items-center">
<h3
id="title"
class="title name shorter flex gap-2"
>
<!-- prettier-ignore -->
{% if community.context.display_name -%}
{{ community.context.display_name }}
{% else %}
{{ community.title }}
{%- endif %}
{% if community.context.is_nsfw -%}
<span
title="NSFW community"
class="flex items-center"
style="color: var(--color-primary)"
>
{{ icon "square-asterisk" }}
</span>
{%- endif %}
</h3>
{% if user -%} {% if user.id != community.owner
%}
<div class="dropdown">
<button
class="camo small"
onclick="trigger('atto::hooks::dropdown', [event])"
exclude="dropdown"
>
{{ icon "ellipsis" }}
</button>
<div class="inner">
<button
class="red"
onclick="trigger('me::report', ['{{ community.id }}', 'community'])"
>
{{ icon "flag" }}
<span
>{{ text "general:action.report"
}}</span
>
</button>
</div>
</div>
{%- endif %} {%- endif %}
</div>
<span class="fade">{{ community.title }}</span>
</div>
</div>
{% if user -%}
<div class="card flex gap-2 flex-wrap" id="join_or_leave">
{% if not is_owner -%} {% if not is_joined -%} {% if not
is_pending %}
<button class="primary" onclick="join_community()">
{{ icon "circle-plus" }}
<span>{{ text "communities:action.join" }}</span>
</button>
<script>
globalThis.join_community = () => {
fetch(
"/api/v1/communities/{{ community.id }}/join",
{
method: "POST",
},
)
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};
</script>
{% else %}
<button
class="quaternary red"
onclick="cancel_request()"
>
{{ icon "x" }}
<span
>{{ text "communities:action.cancel_request"
}}</span
>
</button>
<script>
globalThis.cancel_request = async () => {
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this?",
]))
) {
return;
}
fetch(
"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}",
{
method: "DELETE",
},
)
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};
</script>
{%- endif %} {% else %}
<button
class="quaternary red"
onclick="leave_community()"
>
{{ icon "circle-minus" }}
<span>{{ text "communities:action.leave" }}</span>
</button>
<a
href="/chats/{{ community.id }}/0"
class="button quaternary"
>
{{ icon "message-circle" }}
<span>{{ text "communities:label.chats" }}</span>
</a>
{% if user and can_post -%}
<a
href="/communities/intents/post?community={{ community.id }}"
class="button quaternary"
data-turbo="false"
>
{{ icon "plus" }}
<span>{{ text "general:action.post" }}</span>
</a>
{%- endif %}
<script>
globalThis.leave_community = async () => {
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this?",
]))
) {
return;
}
fetch(
"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}",
{
method: "DELETE",
},
)
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};
</script>
{%- endif %} {% else %}
<a
href="/chats/{{ community.id }}/0"
class="button quaternary"
>
{{ icon "message-circle" }}
<span>{{ text "communities:label.chats" }}</span>
</a>
<a
href="/communities/intents/post?community={{ community.id }}"
class="button quaternary"
data-turbo="false"
>
{{ icon "plus" }}
<span>{{ text "general:action.post" }}</span>
</a>
{%- endif %} {% if can_manage_community or is_manager
-%}
<a
href="/community/{{ community.id }}/manage"
class="button primary"
>
{{ icon "settings" }}
<span
>{{ text "communities:action.configure" }}</span
>
</a>
{%- endif %}
</div>
{%- endif %}
</div>
<div class="card-nest flex flex-col">
<div id="bio" class="card small no_p_margin">
{{ community.context.description|markdown|safe }}
</div>
<div class="card flex flex-col gap-2">
<div class="w-full flex justify-between items-center">
<span class="notification chip">ID</span>
<button
title="Copy"
onclick="trigger('atto::copy_text', ['{{ community.id }}'])"
class="camo small"
>
{{ icon "copy" }}
</button>
</div>
<div class="w-full flex justify-between items-center">
<span class="notification chip">Created</span>
<span class="date">{{ community.created }}</span>
</div>
<div class="w-full flex justify-between items-center">
<span class="notification chip">Members</span>
<a href="/community/{{ community.title }}/members"
>{{ community.member_count }}</a
>
</div>
<div class="w-full flex justify-between items-center">
<span class="notification chip">Score</span>
<div class="flex gap-2">
<b
>{{ community.likes - community.dislikes
}}</b
>
{% if user -%}
<div
class="flex gap-1 reactions_box"
hook="check_reactions"
hook-arg:id="{{ community.id }}"
>
{{ components::likes(id=community.id,
asset_type="Community",
likes=community.likes,
dislikes=community.dislikes) }}
</div>
{%- endif %}
</div>
</div>
</div>
</div>
</div>
<div class="rhs w-full">
{% if can_read -%} {% block content %}{% endblock %} {% else %}
<div class="card-nest">
<div class="card small flex items-center gap-2">
{{ icon "frown" }}
<b
>{{ text "communities:label.not_allowed_to_read"
}}</b
>
</div>
<div class="card">
<span>
{{ text "communities:label.might_need_to_join" }}
</span>
</div>
</div>
{%- endif %}
</div>
</div>
</div>
</article>
{% endblock %}

View file

@ -0,0 +1,300 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "{{ community.context.display_name }} - {{ config.name }}"))
(meta
("name" "og:title")
("content" "{{ community.title }}"))
(meta
("name" "description")
("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!"))
(meta
("name" "og:description")
("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!"))
(meta
("property" "og:type")
("content" "profile"))
(meta
("property" "profile:username")
("content" "{{ community.title }}"))
(meta
("name" "og:image")
("content" "{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar"))
(meta
("name" "twitter:image")
("content" "{{ config.host|safe }}/api/v1/communities/{{ community.id }}/avatar"))
(meta
("name" "twitter:card")
("content" "summary"))
(meta
("name" "twitter:title")
("content" "{{ community.title }}"))
(meta
("name" "twitter:description")
("content" "View the \\\"{{ community.title }}\\\" community on {{ config.name }}!"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(article
(div
("class" "content_container flex flex-col gap-4")
(text "{{ components::community_banner(id=community.id, community=community) }}")
(div
("class" "w-full flex gap-4 flex-collapse")
(div
("class" "lhs flex flex-col gap-2 sm:w-full")
("style" "width: 22rem; min-width: 22rem")
(div
("class" "card-nest w-full")
(div
("class" "card flex gap-2")
("id" "community_avatar_and_name")
(text "{{ components::community_avatar(id=community.id, community=community, size=\"72px\") }}")
(div
("class" "flex flex-col")
(div
("class" "flex gap-2 items-center")
(h3
("id" "title")
("class" "title name shorter flex gap-2")
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %} {% if community.context.is_nsfw -%}")
(span
("title" "NSFW community")
("class" "flex items-center")
("style" "color: var(--color-primary)")
(text "{{ icon \"square-asterisk\" }}"))
(text "{%- endif %}"))
(text "{% if user -%} {% if user.id != community.owner %}")
(div
("class" "dropdown")
(button
("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
(text "{{ icon \"ellipsis\" }}"))
(div
("class" "inner")
(button
("class" "red")
("onclick" "trigger('me::report', ['{{ community.id }}', 'community'])")
(text "{{ icon \"flag\" }}")
(span
(text "{{ text \"general:action.report\" }}")))))
(text "{%- endif %} {%- endif %}"))
(span
("class" "fade")
(text "{{ community.title }}"))))
(text "{% if user -%}")
(div
("class" "card flex gap-2 flex-wrap")
("id" "join_or_leave")
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
(button
("class" "primary")
("onclick" "join_community()")
(text "{{ icon \"circle-plus\" }}")
(span
(text "{{ text \"communities:action.join\" }}")))
(script
(text "globalThis.join_community = () => {
fetch(
\"/api/v1/communities/{{ community.id }}/join\",
{
method: \"POST\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{% else %}")
(button
("class" "quaternary red")
("onclick" "cancel_request()")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"communities:action.cancel_request\" }}")))
(script
(text "globalThis.cancel_request = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
{
method: \"DELETE\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{%- endif %} {% else %}")
(button
("class" "quaternary red")
("onclick" "leave_community()")
(text "{{ icon \"circle-minus\" }}")
(span
(text "{{ text \"communities:action.leave\" }}")))
(a
("href" "/chats/{{ community.id }}/0")
("class" "button quaternary")
(text "{{ icon \"message-circle\" }}")
(span
(text "{{ text \"communities:label.chats\" }}")))
(text "{% if user and can_post -%}")
(a
("href" "/communities/intents/post?community={{ community.id }}")
("class" "button quaternary")
("data-turbo" "false")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"general:action.post\" }}")))
(text "{%- endif %}")
(script
(text "globalThis.leave_community = async () => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(
\"/api/v1/communities/{{ community.id }}/memberships/{{ user.id }}\",
{
method: \"DELETE\",
},
)
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
setTimeout(() => {
window.location.reload();
}, 150);
});
};"))
(text "{%- endif %} {% else %}")
(a
("href" "/chats/{{ community.id }}/0")
("class" "button quaternary")
(text "{{ icon \"message-circle\" }}")
(span
(text "{{ text \"communities:label.chats\" }}")))
(a
("href" "/communities/intents/post?community={{ community.id }}")
("class" "button quaternary")
("data-turbo" "false")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"general:action.post\" }}")))
(text "{%- endif %} {% if can_manage_community or is_manager -%}")
(a
("href" "/community/{{ community.id }}/manage")
("class" "button primary")
(text "{{ icon \"settings\" }}")
(span
(text "{{ text \"communities:action.configure\" }}")))
(text "{%- endif %}"))
(text "{%- endif %}"))
(div
("class" "card-nest flex flex-col")
(div
("id" "bio")
("class" "card small no_p_margin")
(text "{{ community.context.description|markdown|safe }}"))
(div
("class" "card flex flex-col gap-2")
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "ID"))
(button
("title" "Copy")
("onclick" "trigger('atto::copy_text', ['{{ community.id }}'])")
("class" "camo small")
(text "{{ icon \"copy\" }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Created"))
(span
("class" "date")
(text "{{ community.created }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Members"))
(a
("href" "/community/{{ community.title }}/members")
(text "{{ community.member_count }}")))
(div
("class" "w-full flex justify-between items-center")
(span
("class" "notification chip")
(text "Score"))
(div
("class" "flex gap-2")
(b
(text "{{ community.likes - community.dislikes }}"))
(text "{% if user -%}")
(div
("class" "flex gap-1 reactions_box")
("hook" "check_reactions")
("hook-arg:id" "{{ community.id }}")
(text "{{ components::likes(id=community.id, asset_type=\"Community\", likes=community.likes, dislikes=community.dislikes) }}"))
(text "{%- endif %}"))))))
(div
("class" "rhs w-full")
(text "{% if can_read -%} {% block content %}{% endblock %} {% else %}")
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"frown\" }}")
(b
(text "{{ text \"communities:label.not_allowed_to_read\" }}")))
(div
("class" "card")
(span
(text "{{ text \"communities:label.might_need_to_join\" }}"))))
(text "{%- endif %}")))))
(text "{% endblock %}")

View file

@ -1,432 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Create post - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
{% if drafts|length > 0 -%}
<div class="pillmenu">
<a href="#/create" data-tab-button="create" class="active">
{{ icon "plus" }}
<span>{{ text "general:action.post" }}</span>
</a>
<a href="#/drafts" data-tab-button="drafts">
{{ icon "notepad-text-dashed" }}
<span>{{ text "communities:label.drafts" }}</span>
</a>
</div>
{%- endif %}
<div class="card-nest" data-tab="create">
<div class="card small flex items-center justify-between gap-2">
<span class="flex items-center gap-2">
{{ icon "pen" }}
<span>{{ text "communities:label.create_post" }}</span>
</span>
<button onclick="cancel_create_post()" class="quaternary small red">
{{ icon "x" }}
<span>{{ text "dialog:action.cancel" }}</span>
</button>
</div>
<div class="card tertiary flex flex-col gap-2">
{% if draft -%}
<div
class="card secondary w-full flex items-center justify-between gap-2 small"
>
<a class="flex items-center gap-2 flush" href="#/drafts">
{{ icon "notepad-text-dashed" }}
<span class="date">{{ draft.created }}</span>
</a>
<div class="flex gap-2">
<a href="?" class="button quaternary small">
{{ icon "x" }}
<span>{{ text "dialog:action.cancel" }}</span>
</a>
<button
class="button quaternary red small"
onclick="remove_draft('{{ draft.id }}')"
>
{{ icon "trash"}}
<span>{{ text "general:action.delete" }}</span>
</button>
</div>
</div>
{%- endif %} {% if quoting -%}
<div
class="card secondary w-full flex items-center justify-between gap-2 small"
>
<a
class="flex items-center gap-2 flush"
href="/post/{{ quoting[1].id }}"
>
{{ icon "quote" }}
<span>{{ quoting[0].username }}'s post</span>
</a>
<a href="?" class="button quaternary small">
{{ icon "x" }}
<span>{{ text "dialog:action.cancel" }}</span>
</a>
</div>
{%- endif %}
<div class="card-nest">
<div class="card small flex flex-row gap-2 items-center">
{{ components::avatar(username=user.id, size="32px",
selector_type="id") }}
<select
id="community_to_post_to"
onchange="update_community_avatar(event)"
>
<option
value="{{ config.town_square }}"
selected="{% if not selected_community -%}true{% else %}false{%- endif %}"
>
{{ text "auth:link.my_profile" }}
</option>
{% for community in communities %}
<option
value="{{ community.id }}"
selected="{% if selected_community == community.id -%}true{% else %}false{%- endif %}"
>
<!-- prettier-ignore -->
{% if community.context.display_name -%}
{{ community.context.display_name }}
{% else %}
{{ community.title }}
{%- endif %}
</option>
{% endfor %}
</select>
</div>
<form
class="card flex flex-col gap-2"
id="create_form"
onsubmit="create_post_from_form(event)"
>
<div class="flex flex-col gap-1">
<label for="content"
>{{ text "communities:label.content" }}</label
>
<textarea
type="text"
name="content"
id="content"
placeholder="content"
minlength="2"
maxlength="4096"
>
{% if draft -%}{{ draft.content }}{%- endif %}</textarea
>
</div>
<div id="files_list" class="flex gap-2 flex-wrap"></div>
<div class="flex justify-between gap-2">
{{ components::create_post_options() }}
<div class="flex gap-2">
{% if not quoting -%} {% if draft -%}
<button
class="secondary small square"
title="Save as Draft"
onclick="update_draft('{{ draft.id }}')"
type="button"
>
{{ icon "notepad-text-dashed" }}
</button>
{% else %}
<button
class="secondary small square"
title="Save as Draft"
onclick="create_draft()"
type="button"
>
{{ icon "notepad-text-dashed" }}
</button>
{%- endif %} {%- endif %}
<button class="primary">
{{ text "communities:action.create" }}
</button>
</div>
</div>
</form>
</div>
{% if not quoting -%}
<script>
async function create_post_from_form(e) {
e.preventDefault();
await trigger("atto::debounce", ["posts::create"]);
e.target
.querySelector("button.primary")
.classList.add("hidden");
// create body
const body = new FormData();
if (e.target.file_picker) {
for (const file of e.target.file_picker.files) {
body.append(file.name, file);
}
}
body.append(
"body",
JSON.stringify({
content: e.target.content.value,
community: document.getElementById(
"community_to_post_to",
).selectedOptions[0].value,
}),
);
// ...
fetch("/api/v1/posts", {
method: "POST",
body,
})
.then((res) => res.json())
.then(async (res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
// update settings
await update_settings_maybe(res.payload);
// remove draft
// {% if draft -%}
if ("{{ draft.id }}") {
fetch("/api/v1/drafts/{{ draft.id }}", {
method: "DELETE",
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
}
// {%- endif %}
// ...
setTimeout(() => {
window.location.href = `/post/${res.payload}`;
}, 100);
} else {
e.target
.querySelector("button.primary")
.classList.remove("hidden");
}
});
}
async function create_draft() {
const e = {
target: document.getElementById("create_form"),
};
await trigger("atto::debounce", ["posts::create"]);
e.target
.querySelector("button.primary")
.classList.add("hidden");
fetch("/api/v1/drafts", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: e.target.content.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.location.href = `?from_draft=${res.payload}`;
}, 100);
} else {
e.target
.querySelector("button.primary")
.classList.remove("hidden");
}
});
}
async function update_draft(id) {
const e = {
target: document.getElementById("create_form"),
};
await trigger("atto::debounce", ["posts::create"]);
e.target
.querySelector("button.primary")
.classList.add("hidden");
fetch(`/api/v1/drafts/${id}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: e.target.content.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (!res.ok) {
e.target
.querySelector("button.primary")
.classList.remove("hidden");
}
});
}
</script>
{% else %}
<script>
async function create_post_from_form(e) {
const id = await trigger("me::repost", [
"{{ quoting[1].id }}",
e.target.content.value,
document.getElementById("community_to_post_to")
.selectedOptions[0].value,
false,
]);
// update settings
await update_settings_maybe(id);
// redirect
setTimeout(() => {
window.location.href = `/post/${id}`;
}, 100);
}
</script>
{%- endif %}
</div>
</div>
{% if drafts|length > 0 -%}
<div class="card-nest tertiary hidden" data-tab="drafts">
<div class="card small flex items-center gap-2">
{{ icon "notepad-text-dashed" }}
<span>{{ text "communities:label.drafts" }}</span>
</div>
<div class="card flex flex-col gap-2">
{{ components::supporter_ad(body="Become a supporter to save
infinite post drafts!") }} {% for draft in drafts %}
<div class="card-nest">
<div class="card small flex flex-col gap-2">
<span class="no_p_margin"
>{{ draft.content|markdown|safe }}</span
>
<span class="fade date">{{ draft.created }}</span>
</div>
<div class="card flex gap-2 secondary">
<a href="?from_draft={{ draft.id }}" class="button small">
{{ icon "pen-line"}}
<span>{{ text "communities:label.load" }}</span>
</a>
<button
class="button quaternary red small"
onclick="remove_draft('{{ draft.id }}')"
>
{{ icon "trash"}}
<span>{{ text "general:action.delete" }}</span>
</button>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
async function remove_draft(id) {
if (
!(await trigger("atto::confirm", [
"Are you sure you want to do this?",
]))
) {
return;
}
fetch(`/api/v1/drafts/${id}`, {
method: "DELETE",
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
}
</script>
{%- endif %}
</main>
<script>
const town_square = "{{ config.town_square }}";
const user_id = "{{ user.id }}";
function update_community_avatar(e) {
const element = e.target.parentElement.querySelector(".avatar");
const id = e.target.selectedOptions[0].value;
element.setAttribute("title", id);
element.setAttribute("alt", `${id}'s avatar`);
if (id === town_square) {
element.src = `/api/v1/auth/user/${user_id}/avatar?selector_type=id`;
} else {
element.src = `/api/v1/communities/${id}/avatar`;
}
}
setTimeout(() => {
update_community_avatar({
target: document.getElementById("community_to_post_to"),
});
}, 150);
async function cancel_create_post() {
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this? Your post content will be lost.",
]))
) {
return;
}
window.history.back();
}
</script>
{% endblock %}

View file

@ -0,0 +1,406 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Create post - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(text "{% if drafts|length > 0 -%}")
(div
("class" "pillmenu")
(a
("href" "#/create")
("data-tab-button" "create")
("class" "active")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"general:action.post\" }}")))
(a
("href" "#/drafts")
("data-tab-button" "drafts")
(text "{{ icon \"notepad-text-dashed\" }}")
(span
(text "{{ text \"communities:label.drafts\" }}"))))
(text "{%- endif %}")
(div
("class" "card-nest")
("data-tab" "create")
(div
("class" "card small flex items-center justify-between gap-2")
(span
("class" "flex items-center gap-2")
(text "{{ icon \"pen\" }}")
(span
(text "{{ text \"communities:label.create_post\" }}")))
(button
("onclick" "cancel_create_post()")
("class" "quaternary small red")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"dialog:action.cancel\" }}"))))
(div
("class" "card tertiary flex flex-col gap-2")
(text "{% if draft -%}")
(div
("class" "card secondary w-full flex items-center justify-between gap-2 small")
(a
("class" "flex items-center gap-2 flush")
("href" "#/drafts")
(text "{{ icon \"notepad-text-dashed\" }}")
(span
("class" "date")
(text "{{ draft.created }}")))
(div
("class" "flex gap-2")
(a
("href" "?")
("class" "button quaternary small")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"dialog:action.cancel\" }}")))
(button
("class" "button quaternary red small")
("onclick" "remove_draft('{{ draft.id }}')")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"general:action.delete\" }}")))))
(text "{%- endif %} {% if quoting -%}")
(div
("class" "card secondary w-full flex items-center justify-between gap-2 small")
(a
("class" "flex items-center gap-2 flush")
("href" "/post/{{ quoting[1].id }}")
(text "{{ icon \"quote\" }}")
(span
(text "{{ quoting[0].username }}'s post")))
(a
("href" "?")
("class" "button quaternary small")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"dialog:action.cancel\" }}"))))
(text "{%- endif %}")
(div
("class" "card-nest")
(div
("class" "card small flex flex-row gap-2 items-center")
(text "{{ components::avatar(username=user.id, size=\"32px\", selector_type=\"id\") }}")
(select
("id" "community_to_post_to")
("onchange" "update_community_avatar(event)")
(option
("value" "{{ config.town_square }}")
("selected" "{% if not selected_community -%}true{% else %}false{%- endif %}")
(text "{{ text \"auth:link.my_profile\" }}"))
(text "{% for community in communities %}")
(option
("value" "{{ community.id }}")
("selected" "{% if selected_community == community.id -%}true{% else %}false{%- endif %}")
(text "{% if community.context.display_name -%} {{ community.context.display_name }} {% else %} {{ community.title }} {%- endif %}"))
(text "{% endfor %}")))
(form
("class" "card flex flex-col gap-2")
("id" "create_form")
("onsubmit" "create_post_from_form(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "content")
(text "{{ text \"communities:label.content\" }}"))
(textarea
("type" "text")
("name" "content")
("id" "content")
("placeholder" "content")
("minlength" "2")
("maxlength" "4096")
(text "{% if draft -%}{{ draft.content }}{%- endif %}")))
(div
("id" "files_list")
("class" "flex gap-2 flex-wrap"))
(div
("class" "flex justify-between gap-2")
(text "{{ components::create_post_options() }}")
(div
("class" "flex gap-2")
(text "{% if not quoting -%} {% if draft -%}")
(button
("class" "secondary small square")
("title" "Save as Draft")
("onclick" "update_draft('{{ draft.id }}')")
("type" "button")
(text "{{ icon \"notepad-text-dashed\" }}"))
(text "{% else %}")
(button
("class" "secondary small square")
("title" "Save as Draft")
("onclick" "create_draft()")
("type" "button")
(text "{{ icon \"notepad-text-dashed\" }}"))
(text "{%- endif %} {%- endif %}")
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))))
(text "{% if not quoting -%}")
(script
(text "async function create_post_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"posts::create\"]);
e.target
.querySelector(\"button.primary\")
.classList.add(\"hidden\");
// create body
const body = new FormData();
if (e.target.file_picker) {
for (const file of e.target.file_picker.files) {
body.append(file.name, file);
}
}
body.append(
\"body\",
JSON.stringify({
content: e.target.content.value,
community: document.getElementById(
\"community_to_post_to\",
).selectedOptions[0].value,
}),
);
// ...
fetch(\"/api/v1/posts\", {
method: \"POST\",
body,
})
.then((res) => res.json())
.then(async (res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
// update settings
await update_settings_maybe(res.payload);
// remove draft
// {% if draft -%}
if (\"{{ draft.id }}\") {
fetch(\"/api/v1/drafts/{{ draft.id }}\", {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}
// {%- endif %}
// ...
setTimeout(() => {
window.location.href = `/post/${res.payload}`;
}, 100);
} else {
e.target
.querySelector(\"button.primary\")
.classList.remove(\"hidden\");
}
});
}
async function create_draft() {
const e = {
target: document.getElementById(\"create_form\"),
};
await trigger(\"atto::debounce\", [\"posts::create\"]);
e.target
.querySelector(\"button.primary\")
.classList.add(\"hidden\");
fetch(\"/api/v1/drafts\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
content: e.target.content.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.location.href = `?from_draft=${res.payload}`;
}, 100);
} else {
e.target
.querySelector(\"button.primary\")
.classList.remove(\"hidden\");
}
});
}
async function update_draft(id) {
const e = {
target: document.getElementById(\"create_form\"),
};
await trigger(\"atto::debounce\", [\"posts::create\"]);
e.target
.querySelector(\"button.primary\")
.classList.add(\"hidden\");
fetch(`/api/v1/drafts/${id}`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
content: e.target.content.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (!res.ok) {
e.target
.querySelector(\"button.primary\")
.classList.remove(\"hidden\");
}
});
}"))
(text "{% else %}")
(script
(text "async function create_post_from_form(e) {
const id = await trigger(\"me::repost\", [
\"{{ quoting[1].id }}\",
e.target.content.value,
document.getElementById(\"community_to_post_to\")
.selectedOptions[0].value,
false,
]);
// update settings
await update_settings_maybe(id);
// redirect
setTimeout(() => {
window.location.href = `/post/${id}`;
}, 100);
}"))
(text "{%- endif %}")))
(text "{% if drafts|length > 0 -%}")
(div
("class" "card-nest tertiary hidden")
("data-tab" "drafts")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"notepad-text-dashed\" }}")
(span
(text "{{ text \"communities:label.drafts\" }}")))
(div
("class" "card flex flex-col gap-2")
(text "{{ components::supporter_ad(body=\"Become a supporter to save infinite post drafts!\") }} {% for draft in drafts %}")
(div
("class" "card-nest")
(div
("class" "card small flex flex-col gap-2")
(span
("class" "no_p_margin")
(text "{{ draft.content|markdown|safe }}"))
(span
("class" "fade date")
(text "{{ draft.created }}")))
(div
("class" "card flex gap-2 secondary")
(a
("href" "?from_draft={{ draft.id }}")
("class" "button small")
(text "{{ icon \"pen-line\" }}")
(span
(text "{{ text \"communities:label.load\" }}")))
(button
("class" "button quaternary red small")
("onclick" "remove_draft('{{ draft.id }}')")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"general:action.delete\" }}")))))
(text "{% endfor %}")))
(script
(text "async function remove_draft(id) {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you want to do this?\",
]))
) {
return;
}
fetch(`/api/v1/drafts/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}"))
(text "{%- endif %}"))
(script
(text "const town_square = \"{{ config.town_square }}\";
const user_id = \"{{ user.id }}\";
function update_community_avatar(e) {
const element = e.target.parentElement.querySelector(\".avatar\");
const id = e.target.selectedOptions[0].value;
element.setAttribute(\"title\", id);
element.setAttribute(\"alt\", `${id}'s avatar`);
if (id === town_square) {
element.src = `/api/v1/auth/user/${user_id}/avatar?selector_type=id`;
} else {
element.src = `/api/v1/communities/${id}/avatar`;
}
}
setTimeout(() => {
update_community_avatar({
target: document.getElementById(\"community_to_post_to\"),
});
}, 150);
async function cancel_create_post() {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this? Your post content will be lost.\",
]))
) {
return;
}
window.history.back();
}"))
(text "{% endblock %}")

View file

@ -1,45 +0,0 @@
{% import "components.html" as components %} {% extends "communities/base.html"
%} {% block content %}
<div class="flex flex-col gap-4 w-full">
{{ macros::community_nav(community=community, selected="posts") }} {% if
pinned|length != 0 %}
<div class="card-nest">
<div class="card small flex gap-2 items-center">
{{ icon "pin" }}
<span>{{ text "communities:label.pinned" }}</span>
</div>
<div class="card flex flex-col gap-4">
<!-- prettier-ignore -->
{% for post in pinned %}
{% if post[0].context.repost and post[0].context.repost.reposting -%}
{{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }}
{% else %}
{{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }}
{%- endif %}
{% endfor %}
</div>
</div>
{%- endif %}
<div class="card-nest">
<div class="card small flex gap-2 items-center">
{{ icon "newspaper" }}
<span>{{ text "communities:label.posts" }}</span>
</div>
<div class="card flex flex-col gap-4">
<!-- prettier-ignore -->
{% for post in feed %}
{% if post[0].context.repost and post[0].context.repost.reposting -%}
{{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }}
{% else %}
{{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }}
{%- endif %}
{% endfor %}
{{ components::pagination(page=page, items=feed|length) }}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,27 @@
(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}")
(div
("class" "flex flex-col gap-4 w-full")
(text "{{ macros::community_nav(community=community, selected=\"posts\") }} {% if pinned|length != 0 %}")
(div
("class" "card-nest")
(div
("class" "card small flex gap-2 items-center")
(text "{{ icon \"pin\" }}")
(span
(text "{{ text \"communities:label.pinned\" }}")))
(div
("class" "card flex flex-col gap-4")
(text "{% for post in pinned %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {%- endif %} {% endfor %}")))
(text "{%- endif %}")
(div
("class" "card-nest")
(div
("class" "card small flex gap-2 items-center")
(text "{{ icon \"newspaper\" }}")
(span
(text "{{ text \"communities:label.posts\" }}")))
(div
("class" "card flex flex-col gap-4")
(text "{% for post in feed %} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[2], post=post[0], owner=post[1], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[3], secondary=true, show_community=false, can_manage_post=can_manage_posts) }} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=feed|length) }}"))))
(text "{% endblock %}")

View file

@ -1,102 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>My communities - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav(selected="communities") }}
<main class="flex flex-col gap-2">
{% if user -%}
<div class="card-nest">
<div class="card small">
<b>{{ text "communities:label.create_new" }}</b>
</div>
<form
class="card flex flex-col gap-2"
onsubmit="create_community_from_form(event)"
>
<div class="flex flex-col gap-1">
<label for="title">{{ text "communities:label.name" }}</label>
<input
type="text"
name="title"
id="title"
placeholder="name"
required
minlength="2"
maxlength="32"
/>
</div>
<button class="primary">
{{ text "communities:action.create" }}
</button>
</form>
</div>
{% if list|length >= 4 -%} {{ components::supporter_ad(body="Become a
supporter to create up to 10 communities!") }} {%- endif %} {%- endif %}
<div class="card-nest w-full">
<div class="card small flex items-center justify-between gap-2">
<div class="flex items-center gap-2">
{{ icon "award" }}
<span>{{ text "communities:label.my_communities" }}</span>
</div>
<a href="/communities/search" class="button quaternary small">
{{ icon "search" }}
<span>{{ text "communities:label.join_new" }}</span>
</a>
</div>
<div class="card flex flex-col gap-2">
{% for item in list %} {{
components::community_listing_card(community=item) }} {% endfor %}
</div>
</div>
<div class="card-nest w-full">
<div class="card small flex items-center gap-2">
{{ icon "trending-up" }}
<span>{{ text "communities:label.popular_communities" }}</span>
</div>
<div class="card flex flex-col gap-2">
{% for item in popular_list %} {{
components::community_listing_card(community=item) }} {% endfor %}
</div>
</div>
</main>
<script>
async function create_community_from_form(e) {
e.preventDefault();
await trigger("atto::debounce", ["communities::create"]);
if (e.target.title.value.includes(" ")) {
return alert("Cannot contain spaces!");
}
fetch("/api/v1/communities", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
title: e.target.title.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.location.href = `/community/${res.payload}`;
}, 100);
}
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,97 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "My communities - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}")
(main
("class" "flex flex-col gap-2")
(text "{% if user -%}")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ text \"communities:label.create_new\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "create_community_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")))
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))
(text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}")
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center justify-between gap-2")
(div
("class" "flex items-center gap-2")
(text "{{ icon \"award\" }}")
(span
(text "{{ text \"communities:label.my_communities\" }}")))
(a
("href" "/communities/search")
("class" "button quaternary small")
(text "{{ icon \"search\" }}")
(span
(text "{{ text \"communities:label.join_new\" }}"))))
(div
("class" "card flex flex-col gap-2")
(text "{% for item in list %} {{ components::community_listing_card(community=item) }} {% endfor %}")))
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"trending-up\" }}")
(span
(text "{{ text \"communities:label.popular_communities\" }}")))
(div
("class" "card flex flex-col gap-2")
(text "{% for item in popular_list %} {{ components::community_listing_card(community=item) }} {% endfor %}"))))
(script
(text "async function create_community_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"communities::create\"]);
if (e.target.title.value.includes(\" \")) {
return alert(\"Cannot contain spaces!\");
}
fetch(\"/api/v1/communities\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
title: e.target.title.value,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.location.href = `/community/${res.payload}`;
}, 100);
}
});
}"))
(text "{% endblock %}")

View file

@ -1,49 +0,0 @@
{% import "components.html" as components %} {% extends "communities/base.html"
%} {% block content %}
<div class="flex flex-col gap-4 w-full">
<div class="card-nest">
<div class="card small flex gap-2 items-center">
{{ icon "users-round" }}
<span>{{ text "communities:tab.members" }}</span>
</div>
<div class="card flex flex-col gap-4">
{% if page == 0 -%}
<div class="card-nest">
<div class="card small flex items-center gap-2">
{{ icon "crown" }}
<span>Owner</span>
</div>
{{ components::user_card(user=owner) }}
</div>
{%- endif %}
<!-- prettier-ignore -->
{% for item in list %}
<div class="card-nest">
<div class="card small flex items-center gap-2 justify-between">
<span>
Since
<span class="date">{{ item[0].created }}</span>
</span>
{% if can_manage_roles -%}
<a
href="/community/{{ community.id }}/manage?uid={{ item[1].id }}#/members"
class="button small quaternary"
>
{{ icon "pencil" }}
<span>{{ text "general:action.manage" }}</span>
</a>
{%- endif %}
</div>
{{ components::user_card(user=item[1]) }}
</div>
{% endfor %} {{ components::pagination(page=page, items=list|length)
}}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,44 @@
(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}")
(div
("class" "flex flex-col gap-4 w-full")
(div
("class" "card-nest")
(div
("class" "card small flex gap-2 items-center")
(text "{{ icon \"users-round\" }}")
(span
(text "{{ text \"communities:tab.members\" }}")))
(div
("class" "card flex flex-col gap-4")
(text "{% if page == 0 -%}")
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"crown\" }}")
(span
(text "Owner")))
(text "{{ components::user_card(user=owner) }}"))
(text "{%- endif %}")
(text "{% for item in list %}")
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2 justify-between")
(span
(text "Since")
(span
("class" "date")
(text "{{ item[0].created }}")))
(text "{% if can_manage_roles -%}")
(a
("href" "/community/{{ community.id }}/manage?uid={{ item[1].id }}#/members")
("class" "button small quaternary")
(text "{{ icon \"pencil\" }}")
(span
(text "{{ text \"general:action.manage\" }}")))
(text "{%- endif %}"))
(text "{{ components::user_card(user=item[1]) }}"))
(text "{% endfor %} {{ components::pagination(page=page, items=list|length) }}"))))
(text "{% endblock %}")

View file

@ -1,111 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Question - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div style="display: contents">
{{ components::question(question=question, owner=owner) }}
</div>
{% if user and (user.id == question.receiver or question.is_global) and not
has_answered %}
<div class="card-nest">
<div class="card small flex items-center gap-2">
{{ icon "square-pen" }}
<b>{{ text "requests:label.answer" }}</b>
</div>
<form
class="card flex flex-col gap-2"
onsubmit="answer_question_from_form(event, '{{ question.id }}')"
>
<div class="flex flex-col gap-1">
<label for="content"
>{{ text "communities:label.content" }}</label
>
<textarea
type="text"
name="content"
id="content"
placeholder="content"
required
minlength="2"
maxlength="4096"
></textarea>
</div>
<div id="files_list" class="flex gap-2 flex-wrap"></div>
<div class="flex gap-2">
{{ components::emoji_picker(element_id="content",
render_dialog=true) }} {% if is_supporter -%} {{
components::file_picker(files_list_id="files_list") }} {% endif
%}
<button class="primary">
{{ text "requests:label.answer" }}
</button>
</div>
</form>
</div>
{%- endif %}
<div class="card-nest w-full" data-tab="replies">
<div class="card small flex items-center gap-2">
{{ icon "newspaper" }}
<span>{{ text "communities:label.replies" }}</span>
</div>
<div class="card flex flex-col gap-4">
<!-- prettier-ignore -->
{% for post in replies %}
{{ components::post(post=post[0], owner=post[1], question=false, secondary=true, show_community=false) }}
{% endfor %}
{{ components::pagination(page=page, items=replies|length) }}
</div>
</div>
</main>
<script>
const community = "{{ question.community }}";
window.answer_question_from_form = async (e, answering) => {
e.preventDefault();
await trigger("atto::debounce", ["posts::create"]);
// create body
const body = new FormData();
if (e.target.file_picker) {
for (const file of e.target.file_picker.files) {
body.append(file.name, file);
}
}
body.append(
"body",
JSON.stringify({
content: e.target.content.value,
community: community ? community : "{{ config.town_square }}",
answering,
}),
);
// ...
fetch("/api/v1/posts", {
method: "POST",
body,
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
};
</script>
{% endblock %}

View file

@ -0,0 +1,99 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Question - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("style" "display: contents")
(text "{{ components::question(question=question, owner=owner) }}"))
(text "{% if user and (user.id == question.receiver or question.is_global) and not has_answered %}")
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"square-pen\" }}")
(b
(text "{{ text \"requests:label.answer\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "answer_question_from_form(event, '{{ question.id }}')")
(div
("class" "flex flex-col gap-1")
(label
("for" "content")
(text "{{ text \"communities:label.content\" }}"))
(textarea
("type" "text")
("name" "content")
("id" "content")
("placeholder" "content")
("required" "")
("minlength" "2")
("maxlength" "4096")))
(div
("id" "files_list")
("class" "flex gap-2 flex-wrap"))
(div
("class" "flex gap-2")
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
(button
("class" "primary")
(text "{{ text \"requests:label.answer\" }}")))))
(text "{%- endif %}")
(div
("class" "card-nest w-full")
("data-tab" "replies")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"newspaper\" }}")
(span
(text "{{ text \"communities:label.replies\" }}")))
(div
("class" "card flex flex-col gap-4")
(text "{% for post in replies %} {{ components::post(post=post[0], owner=post[1], question=false, secondary=true, show_community=false) }} {% endfor %} {{ components::pagination(page=page, items=replies|length) }}"))))
(script
(text "const community = \"{{ question.community }}\";
window.answer_question_from_form = async (e, answering) => {
e.preventDefault();
await trigger(\"atto::debounce\", [\"posts::create\"]);
// create body
const body = new FormData();
if (e.target.file_picker) {
for (const file of e.target.file_picker.files) {
body.append(file.name, file);
}
}
body.append(
\"body\",
JSON.stringify({
content: e.target.content.value,
community: community ? community : \"{{ config.town_square }}\",
answering,
}),
);
// ...
fetch(\"/api/v1/posts\", {
method: \"POST\",
body,
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
};"))
(text "{% endblock %}")

View file

@ -1,30 +0,0 @@
{% import "components.html" as components %} {% extends "communities/base.html"
%} {% block content %}
<div class="flex flex-col gap-4 w-full">
{{ macros::community_nav(community=community, selected="questions") }}
<!-- prettier-ignore -->
{% if user and can_post -%}
<div style="display: contents">
{{ components::create_question_form(community=community.id,
is_global=true) }}
</div>
{%- endif %}
<div class="card-nest">
<div class="card small flex gap-2 items-center">
{{ icon "newspaper" }}
<span>{{ text "communities:label.questions" }}</span>
</div>
<div class="card flex flex-col gap-4">
<!-- prettier-ignore -->
{% for question in feed %}
{{ components::global_question(question=question, can_manage_questions=can_manage_questions, show_community=false, secondary=true) }}
{% endfor %}
{{ components::pagination(page=page, items=feed|length) }}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,21 @@
(text "{% import \"components.html\" as components %} {% extends \"communities/base.html\" %} {% block content %}")
(div
("class" "flex flex-col gap-4 w-full")
(text "{{ macros::community_nav(community=community, selected=\"questions\") }}")
(text "{% if user and can_post -%}")
(div
("style" "display: contents")
(text "{{ components::create_question_form(community=community.id, is_global=true) }}"))
(text "{%- endif %}")
(div
("class" "card-nest")
(div
("class" "card small flex gap-2 items-center")
(text "{{ icon \"newspaper\" }}")
(span
(text "{{ text \"communities:label.questions\" }}")))
(div
("class" "card flex flex-col gap-4")
(text "{% for question in feed %} {{ components::global_question(question=question, can_manage_questions=can_manage_questions, show_community=false, secondary=true) }} {% endfor %} {{ components::pagination(page=page, items=feed|length) }}"))))
(text "{% endblock %}")

View file

@ -1,45 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Search communities - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav(selected="communities") }}
<main class="flex flex-col gap-2">
<div class="card-nest">
<div class="card small flex items-center gap-2">
{{ icon "search" }}
<span>{{ text "general:link.search" }}</span>
</div>
<form class="card flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label for="text">{{ text "communities:label.query" }}</label>
<input
type="text"
name="text"
id="text"
placeholder="text"
required
maxlength="32"
value="{{ text }}"
/>
</div>
<button class="primary">{{ text "dialog:action.continue" }}</button>
</form>
</div>
<div class="card-nest">
<div class="card small flex items-center gap-2">
{{ icon "book-marked" }}
<span>{{ text "communities:label.search_results" }}</span>
</div>
<!-- prettier-ignore -->
<div class="card flex flex-col gap-4">
{% for item in list %}
{{ components::community_listing_card(community=item) }}
{% endfor %}
{{ components::pagination(page=page, items=list|length, key="&text=", value=text) }}
</div>
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,44 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Search communities - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"communities\") }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"search\" }}")
(span
(text "{{ text \"general:link.search\" }}")))
(form
("class" "card flex flex-col gap-4")
(div
("class" "flex flex-col gap-1")
(label
("for" "text")
(text "{{ text \"communities:label.query\" }}"))
(input
("type" "text")
("name" "text")
("id" "text")
("placeholder" "text")
("required" "")
("maxlength" "32")
("value" "{{ text }}")))
(button
("class" "primary")
(text "{{ text \"dialog:action.continue\" }}"))))
(div
("class" "card-nest")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"book-marked\" }}")
(span
(text "{{ text \"communities:label.search_results\" }}")))
(div
("class" "card flex flex-col gap-4")
(text "{% for item in list %} {{ components::community_listing_card(community=item) }} {% endfor %} {{ components::pagination(page=page, items=list|length, key=\"&text=\", value=text) }}"))))
(text "{% endblock %}")

View file

@ -1,964 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Community settings - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div class="pillmenu">
<a href="#/general" data-tab-button="general" class="active">
{{ icon "settings" }}
<span>{{ text "settings:tab.general" }}</span>
</a>
<a href="#/images" data-tab-button="images">
{{ icon "image" }}
<span>{{ text "settings:tab.images" }}</span>
</a>
<a href="#/members" data-tab-button="members">
{{ icon "users-round" }}
<span>{{ text "communities:tab.members" }}</span>
</a>
{% if can_manage_channels -%}
<a href="#/channels" data-tab-button="channels">
{{ icon "rss" }}
<span>{{ text "communities:tab.channels" }}</span>
</a>
{%- endif %} {% if can_manage_emojis -%}
<a href="#/emojis" data-tab-button="emojis">
{{ icon "smile" }}
<span>{{ text "communities:tab.emojis" }}</span>
</a>
{%- endif %}
</div>
<div class="w-full flex flex-col gap-2" data-tab="general">
<div id="manage_fields" class="card tertiary flex flex-col gap-2">
<div class="card-nest" ui_ident="read_access">
<div class="card small">
<b>Read access</b>
</div>
<div class="card">
<select onchange="save_access(event, 'read')">
<option
value="Everybody"
selected="{% if community.read_access == 'Everybody' -%}true{% else %}false{%- endif %}"
>
Everybody
</option>
<option
value="Joined"
selected="{% if community.read_access == 'Joined' -%}true{% else %}false{%- endif %}"
>
Joined
</option>
</select>
</div>
</div>
<div class="card-nest" ui_ident="join_access">
<div class="card small">
<b>Join access</b>
</div>
<div class="card">
<select onchange="save_access(event, 'join')">
<option
value="Everybody"
selected="{% if community.join_access == 'Everybody' -%}true{% else %}false{%- endif %}"
>
Everybody
</option>
<option
value="Request"
selected="{% if community.join_access == 'Request' -%}true{% else %}false{%- endif %}"
>
Request
</option>
<option
value="Nobody"
selected="{% if community.join_access == 'Nobody' -%}true{% else %}false{%- endif %}"
>
Nobody
</option>
</select>
</div>
</div>
<div class="card-nest" ui_ident="write_access">
<div class="card small">
<b>Post permission</b>
</div>
<div class="card">
<select onchange="save_access(event, 'write')">
<option
value="Everybody"
selected="{% if community.write_access == 'Everybody' -%}true{% else %}false{%- endif %}"
>
Everybody
</option>
<option
value="Joined"
selected="{% if community.write_access == 'Joined' -%}true{% else %}false{%- endif %}"
>
Joined
</option>
<option
value="Owner"
selected="{% if community.write_access == 'Owner' -%}true{% else %}false{%- endif %}"
>
Owner only
</option>
</select>
</div>
</div>
<div class="card-nest" ui_ident="change_title">
<div class="card small">
<b>{{ text "communities:label.change_title" }}</b>
</div>
<form
class="card flex flex-col gap-2"
onsubmit="change_title(event)"
>
<div class="flex flex-col gap-1">
<label for="new_title"
>{{ text "communities:label.new_title" }}</label
>
<input
type="text"
name="new_title"
id="new_title"
placeholder="new_title"
required
minlength="2"
/>
</div>
<button class="primary">
{{ icon "check" }}
<span>{{ text "general:action.save" }}</span>
</button>
</form>
</div>
</div>
<div class="card-nest" ui_ident="danger_zone">
<div class="card small flex gap-1 items-center red">
{{ icon "skull" }}
<b> {{ text "communities:label.danger_zone" }} </b>
</div>
<div class="card flex flex-wrap gap-2">
<button class="red quaternary" onclick="delete_community()">
{{ icon "trash" }}
<span>{{ text "communities:label.delete_community" }}</span>
</button>
</div>
</div>
<div class="flex gap-2 flex-wrap">
<button onclick="save_context()">
{{ icon "check" }}
<span>{{ text "general:action.save" }}</span>
</button>
<a href="/community/{{ community.title }}" class="button secondary">
{{ icon "arrow-left" }}
<span>{{ text "general:action.back" }}</span>
</a>
</div>
</div>
<div
class="card tertiary w-full hidden flex flex-col gap-2"
data-tab="images"
>
<div class="card-nest" ui_ident="change_avatar">
<div class="card small">
<b>{{ text "settings:label.change_avatar" }}</b>
</div>
<form
class="card flex gap-2 flex-row flex-wrap items-center"
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"
class="w-content"
/>
<button class="primary">{{ icon "check" }}</button>
</form>
</div>
<div class="card-nest" ui_ident="change_banner">
<div class="card small">
<b>{{ text "settings:label.change_banner" }}</b>
</div>
<form
class="card flex flex-col gap-2"
method="post"
enctype="multipart/form-data"
onsubmit="upload_banner(event)"
>
<div class="flex gap-2 flex-row flex-wrap items-center">
<input
id="banner_file"
name="file"
type="file"
accept="image/png,image/jpeg,image/avif,image/webp"
class="w-content"
/>
<button class="primary">{{ icon "check" }}</button>
</div>
<span class="fade"
>Use an image of 1100x350px for the best results.</span
>
</form>
</div>
</div>
<div
class="card tertiary w-full hidden flex flex-col gap-2"
data-tab="members"
>
<div class="card-nest">
<div class="card small">
<b>{{ text "communities:label.select_member" }}</b>
</div>
<form
class="card flex-col gap-2"
onsubmit="select_user_from_form(event)"
>
<div class="flex flex-col gap-1">
<div class="flex flex-col gap-1">
<label for="uid"
>{{ text "communities:label.user_id" }}</label
>
<input
type="number"
name="uid"
id="uid"
placeholder="user id"
required
minlength="18"
/>
</div>
<button class="primary">
{{ text "communities:action.select" }}
</button>
</div>
</form>
</div>
<div class="card flex flex-col gap-2 w-full" id="membership_info"></div>
</div>
{% if can_manage_channels -%}
<div
class="card tertiary w-full hidden flex flex-col gap-2"
data-tab="channels"
>
<div class="card-nest">
<div class="card small">
<b>{{ text "communities:action.create_channel" }}</b>
</div>
<form
class="card flex flex-col gap-2"
onsubmit="create_channel_from_form(event)"
>
<div class="flex flex-col gap-1">
<label for="title"
>{{ text "communities:label.name" }}</label
>
<input
type="text"
name="title"
id="title"
placeholder="name"
required
minlength="2"
maxlength="32"
/>
</div>
<button class="primary">
{{ text "communities:action.create" }}
</button>
</form>
</div>
{% for channel in channels %}
<div class="card-nest">
<div class="card small">
<b>{{ channel.position }}</b>
{{ channel.title }}
</div>
<div class="card flex gap-2">
<button
class="red quaternary small"
onclick="delete_channel('{{ channel.id }}')"
>
{{ text "general:action.delete" }}
</button>
<button
class="quaternary small"
onclick="update_channel_position('{{ channel.id }}')"
>
{{ text "chats:action.move" }}
</button>
<button
class="quaternary small"
onclick="update_channel_title('{{ channel.id }}')"
>
{{ text "chats:action.rename" }}
</button>
</div>
</div>
{% endfor %}
</div>
<script>
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();
}
});
}
</script>
{%- endif %} {% if can_manage_emojis -%}
<div
class="card tertiary w-full hidden flex flex-col gap-2"
data-tab="emojis"
>
{{ components::supporter_ad(body="Become a supporter to upload GIF
animated emojis!") }}
<div class="card-nest" ui_ident="change_banner">
<div class="card small flex items-center gap-2">
{{ icon "upload" }}
<b>{{ text "communities:label.upload" }}</b>
</div>
<form
class="card flex flex-col gap-2"
onsubmit="upload_emoji(event)"
>
<div class="flex flex-col gap-1">
<label for="name"
>{{ text "communities:label.name" }}</label
>
<input
type="text"
name="name"
id="name"
placeholder="name"
required
minlength="2"
maxlength="32"
/>
</div>
<div class="flex flex-col gap-1">
<label for="file"
>{{ text "communities:label.file" }}</label
>
<input
id="banner_file"
name="file"
type="file"
accept="image/png,image/jpeg,image/avif,image/webp"
class="w-full"
/>
</div>
<button>{{ text "communities:action.create" }}</button>
<span class="fade"
>Emojis can be a maximum of 256 KiB, or 512x512px (width x
height).</span
>
</form>
</div>
{% for emoji in emojis %}
<div
class="card secondary flex flex-wrap gap-2 items-center justify-between"
>
<div class="flex gap-2 items-center">
<img
src="/api/v1/communities/{{ community.id }}/emojis/{{ emoji.name }}"
alt="{{ emoji.name }}"
class="emoji"
loading="lazy"
/>
<b>{{ emoji.name }}</b>
</div>
<div class="flex gap-2">
<button
class="quaternary small"
onclick="rename_emoji('{{ emoji.id }}')"
>
{{ icon "pencil" }}
<span>{{ text "chats:action.rename" }}</span>
</button>
<button
class="quaternary small red"
onclick="remove_emoji('{{ emoji.id }}')"
>
{{ icon "x" }}
<span>{{ text "stacks:label.remove" }}</span>
</button>
</div>
</div>
{% endfor %}
</div>
<script>
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,
]);
});
};
</script>
{%- endif %}
</main>
<script>
setTimeout(() => {
const element = document.getElementById("membership_info");
const ui = ns("ui");
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", [
"Are you sure you would like to do this?\n\nThis action is PERMANENT!",
]))
) {
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((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (!res.ok) {
return;
}
// permissions manager
const get_permissions_html = trigger(
"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,
},
],
);
// ...
element.innerHTML = `<div class="flex gap-2 flex-wrap" ui_ident="actions">
<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 quaternary" onclick="update_user_role('${e.target.uid.value}', 33)">Ban</button>` : `<button class="quaternary" onclick="update_user_role('${e.target.uid.value}', 5)">Unban</button>`}
${res.payload.role !== 65 ? `<button class="red quaternary" onclick="update_user_role('${e.target.uid.value}', 65)">Send to review</button>` : `<button class="green quaternary" onclick="update_user_role('${e.target.uid.value}', 5)">Accept join request</button>`}
<button class="red quaternary" onclick="kick_user('${e.target.uid.value}')">Kick</button>
<button class="red quaternary" onclick="transfer_ownership('${e.target.uid.value}')">Transfer ownership</button>
</div>
<div class="flex flex-col gap-2" ui_ident="permissions" id="permissions">
${get_permissions_html(res.payload.role, "permissions")}
</div>`;
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>
<!-- prettier-ignore -->
<script type="application/json" id="settings_json">{{ community.context|json_encode()|safe }}</script>
<script>
setTimeout(() => {
const ui = ns("ui");
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",
]);
ui.generate_settings_ui(
document.getElementById("manage_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",
],
[
[
"enable_questions",
"Allow users to ask questions in this community",
],
"{{ community.context.enable_questions }}",
"checkbox",
],
],
settings,
);
}, 250);
</script>
{% endblock %}

View file

@ -0,0 +1,912 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Community settings - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "pillmenu")
(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\" }}")))
(text "{% if can_manage_channels -%}")
(a
("href" "#/channels")
("data-tab-button" "channels")
(text "{{ icon \"rss\" }}")
(span
(text "{{ text \"communities:tab.channels\" }}")))
(text "{%- endif %} {% if can_manage_emojis -%}")
(a
("href" "#/emojis")
("data-tab-button" "emojis")
(text "{{ icon \"smile\" }}")
(span
(text "{{ text \"communities:tab.emojis\" }}")))
(text "{%- endif %}"))
(div
("class" "w-full flex flex-col gap-2")
("data-tab" "general")
(div
("id" "manage_fields")
("class" "card tertiary flex flex-col gap-2")
(div
("class" "card-nest")
("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
("class" "card-nest")
("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
("class" "card-nest")
("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
("class" "card-nest")
("ui_ident" "change_title")
(div
("class" "card small")
(b
(text "{{ text \"communities:label.change_title\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "change_title(event)")
(div
("class" "flex flex-col gap-1")
(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
("class" "primary")
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}"))))))
(div
("class" "card-nest")
("ui_ident" "danger_zone")
(div
("class" "card small flex gap-1 items-center red")
(text "{{ icon \"skull\" }}")
(b
(text "{{ text \"communities:label.danger_zone\" }}")))
(div
("class" "card flex flex-wrap gap-2")
(button
("class" "red quaternary")
("onclick" "delete_community()")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"communities:label.delete_community\" }}")))))
(div
("class" "flex gap-2 flex-wrap")
(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
("class" "card tertiary w-full hidden flex flex-col gap-2")
("data-tab" "images")
(div
("class" "card-nest")
("ui_ident" "change_avatar")
(div
("class" "card small")
(b
(text "{{ text \"settings:label.change_avatar\" }}")))
(form
("class" "card flex gap-2 flex-row flex-wrap items-center")
("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")
("class" "w-content"))
(button
("class" "primary")
(text "{{ icon \"check\" }}"))))
(div
("class" "card-nest")
("ui_ident" "change_banner")
(div
("class" "card small")
(b
(text "{{ text \"settings:label.change_banner\" }}")))
(form
("class" "card flex flex-col gap-2")
("method" "post")
("enctype" "multipart/form-data")
("onsubmit" "upload_banner(event)")
(div
("class" "flex gap-2 flex-row flex-wrap items-center")
(input
("id" "banner_file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp")
("class" "w-content"))
(button
("class" "primary")
(text "{{ icon \"check\" }}")))
(span
("class" "fade")
(text "Use an image of 1100x350px for the best results.")))))
(div
("class" "card tertiary w-full hidden flex flex-col gap-2")
("data-tab" "members")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ text \"communities:label.select_member\" }}")))
(form
("class" "card flex-col gap-2")
("onsubmit" "select_user_from_form(event)")
(div
("class" "flex flex-col gap-1")
(div
("class" "flex flex-col gap-1")
(label
("for" "uid")
(text "{{ text \"communities:label.user_id\" }}"))
(input
("type" "number")
("name" "uid")
("id" "uid")
("placeholder" "user id")
("required" "")
("minlength" "18")))
(button
("class" "primary")
(text "{{ text \"communities:action.select\" }}")))))
(div
("class" "card flex flex-col gap-2 w-full")
("id" "membership_info")))
(text "{% if can_manage_channels -%}")
(div
("class" "card tertiary w-full hidden flex flex-col gap-2")
("data-tab" "channels")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ text \"communities:action.create_channel\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "create_channel_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")))
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))
(text "{% for channel in channels %}")
(div
("class" "card-nest")
(div
("class" "card small")
(b
(text "{{ channel.position }} "))
(text "{{ channel.title }}"))
(div
("class" "card flex gap-2")
(button
("class" "red quaternary small")
("onclick" "delete_channel('{{ channel.id }}')")
(text "{{ text \"general:action.delete\" }}"))
(button
("class" "quaternary small")
("onclick" "update_channel_position('{{ channel.id }}')")
(text "{{ text \"chats:action.move\" }}"))
(button
("class" "quaternary small")
("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
("class" "card tertiary w-full hidden flex flex-col gap-2")
("data-tab" "emojis")
(text "{{ components::supporter_ad(body=\"Become a supporter to upload GIF animated emojis!\") }}")
(div
("class" "card-nest")
("ui_ident" "change_banner")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"upload\" }}")
(b
(text "{{ text \"communities:label.upload\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "upload_emoji(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "name")
(text "{{ text \"communities:label.name\" }}"))
(input
("type" "text")
("name" "name")
("id" "name")
("placeholder" "name")
("required" "")
("minlength" "2")
("maxlength" "32")))
(div
("class" "flex flex-col gap-1")
(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")
("class" "w-full")))
(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
("class" "card secondary flex flex-wrap gap-2 items-center justify-between")
(div
("class" "flex gap-2 items-center")
(img
("src" "/api/v1/communities/{{ community.id }}/emojis/{{ emoji.name }}")
("alt" "{{ emoji.name }}")
("class" "emoji")
("loading" "lazy"))
(b
(text "{{ emoji.name }}")))
(div
("class" "flex gap-2")
(button
("class" "quaternary small")
("onclick" "rename_emoji('{{ emoji.id }}')")
(text "{{ icon \"pencil\" }}")
(span
(text "{{ text \"chats:action.rename\" }}")))
(button
("class" "quaternary small red")
("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,
]);
});
};"))
(text "{%- endif %}"))
(script
(text "setTimeout(() => {
const element = document.getElementById(\"membership_info\");
const ui = ns(\"ui\");
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\", [
\"Are you sure you would like to do this?\n\nThis action is PERMANENT!\",
]))
) {
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((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (!res.ok) {
return;
}
// permissions manager
const get_permissions_html = trigger(
\"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,
},
],
);
// ...
element.innerHTML = `<div class=\"flex gap-2 flex-wrap\" ui_ident=\"actions\">
<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 quaternary\" onclick=\"update_user_role('${e.target.uid.value}', 33)\">Ban</button>` : `<button class=\"quaternary\" onclick=\"update_user_role('${e.target.uid.value}', 5)\">Unban</button>`}
${res.payload.role !== 65 ? `<button class=\"red quaternary\" onclick=\"update_user_role('${e.target.uid.value}', 65)\">Send to review</button>` : `<button class=\"green quaternary\" onclick=\"update_user_role('${e.target.uid.value}', 5)\">Accept join request</button>`}
<button class=\"red quaternary\" onclick=\"kick_user('${e.target.uid.value}')\">Kick</button>
<button class=\"red quaternary\" onclick=\"transfer_ownership('${e.target.uid.value}')\">Transfer ownership</button>
</div>
<div class=\"flex flex-col gap-2\" ui_ident=\"permissions\" id=\"permissions\">
${get_permissions_html(res.payload.role, \"permissions\")}
</div>`;
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(() => {
const ui = ns(\"ui\");
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\",
]);
ui.generate_settings_ui(
document.getElementById(\"manage_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\",
],
[
[
\"enable_questions\",
\"Allow users to ask questions in this community\",
],
\"{{ community.context.enable_questions }}\",
\"checkbox\",
],
],
settings,
);
}, 250);"))
(text "{% endblock %}")