add: finish ui rewrite
This commit is contained in:
parent
e9846016e6
commit
5dec98d698
119 changed files with 8776 additions and 9350 deletions
|
@ -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 %}
|
300
crates/app/src/public/html/communities/base.lisp
Normal file
300
crates/app/src/public/html/communities/base.lisp
Normal 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 %}")
|
|
@ -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 %}
|
406
crates/app/src/public/html/communities/create_post.lisp
Normal file
406
crates/app/src/public/html/communities/create_post.lisp
Normal 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 %}")
|
|
@ -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 %}
|
27
crates/app/src/public/html/communities/feed.lisp
Normal file
27
crates/app/src/public/html/communities/feed.lisp
Normal 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 %}")
|
|
@ -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 %}
|
97
crates/app/src/public/html/communities/list.lisp
Normal file
97
crates/app/src/public/html/communities/list.lisp
Normal 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 %}")
|
|
@ -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 %}
|
44
crates/app/src/public/html/communities/members.lisp
Normal file
44
crates/app/src/public/html/communities/members.lisp
Normal 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 %}")
|
|
@ -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 %}
|
99
crates/app/src/public/html/communities/question.lisp
Normal file
99
crates/app/src/public/html/communities/question.lisp
Normal 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 %}")
|
|
@ -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 %}
|
21
crates/app/src/public/html/communities/questions.lisp
Normal file
21
crates/app/src/public/html/communities/questions.lisp
Normal 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 %}")
|
|
@ -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 %}
|
44
crates/app/src/public/html/communities/search.lisp
Normal file
44
crates/app/src/public/html/communities/search.lisp
Normal 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 %}")
|
|
@ -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 %}
|
912
crates/app/src/public/html/communities/settings.lisp
Normal file
912
crates/app/src/public/html/communities/settings.lisp
Normal 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 %}")
|
Loading…
Add table
Add a link
Reference in a new issue