add: finish ui rewrite

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

View file

@ -1,38 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Audit log - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div class="card-nest w-full">
<div class="card small flex items-center gap-2">
{{ icon "scroll" }}
<span>{{ text "general:link.audit_log" }}</span>
</div>
<div class="card flex flex-col gap-2">
<!-- prettier-ignore -->
{% for item in items %}
<div class="card-nest">
<a
class="card small flex items-center gap-2 flush"
href="/api/v1/auth/user/find/{{ item.moderator }}"
>
<!-- prettier-ignore -->
{{ components::avatar(username=item.moderator, selector_type="id") }}
<span>{{ item.moderator }}</span>
<span class="fade date">{{ item.created }}</span>
</a>
<div class="card secondary">
<span class="no_p_margin"
>{{ item.content|markdown|safe }}</span
>
</div>
</div>
{% endfor %}
<!-- prettier-ignore -->
{{ components::pagination(page=page, items=items|length) }}
</div>
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,37 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Audit log - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"scroll\" }}")
(span
(text "{{ text \"general:link.audit_log\" }}")))
(div
("class" "card flex flex-col gap-2")
(text "{% for item in items %}")
(div
("class" "card-nest")
(a
("class" "card small flex items-center gap-2 flush")
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
(text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}")
(span
(text "{{ item.moderator }}"))
(span
("class" "fade date")
(text "{{ item.created }}")))
(div
("class" "card secondary")
(span
("class" "no_p_margin")
(text "{{ item.content|markdown|safe }}"))))
(text "{% endfor %}")
(text "{{ components::pagination(page=page, items=items|length) }}"))))
(text "{% endblock %}")

View file

@ -1,65 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>File report - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div class="card-nest w-full">
<div class="card small flex items-center gap-2">
{{ icon "flag" }}
<span>{{ text "general:label.file_report" }}</span>
</div>
<form
class="card flex flex-col gap-2"
onsubmit="create_report_from_form(event)"
>
<div class="flex flex-col gap-1">
<label for="title"
>{{ text "communities:label.content" }}</label
>
<textarea
type="text"
name="content"
id="content"
placeholder="content"
required
minlength="16"
></textarea>
</div>
<button class="primary">
{{ text "communities:action.create" }}
</button>
</form>
</div>
</main>
<script>
function create_report_from_form(e) {
e.preventDefault();
fetch("/api/v1/reports", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
content: e.target.content.value,
asset: "{{ asset }}",
asset_type: `{{ asset_type }}`,
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.close();
}, 150);
}
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,63 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "File report - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"flag\" }}")
(span
(text "{{ text \"general:label.file_report\" }}")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "create_report_from_form(event)")
(div
("class" "flex flex-col gap-1")
(label
("for" "title")
(text "{{ text \"communities:label.content\" }}"))
(textarea
("type" "text")
("name" "content")
("id" "content")
("placeholder" "content")
("required" "")
("minlength" "16")))
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))))
(script
(text "function create_report_from_form(e) {
e.preventDefault();
fetch(\"/api/v1/reports\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
content: e.target.content.value,
asset: \"{{ asset }}\",
asset_type: `{{ asset_type }}`,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
setTimeout(() => {
window.close();
}, 150);
}
});
}"))
(text "{% endblock %}")

View file

@ -1,87 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>IP Bans - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<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 "ban" }}
<span>{{ text "general:link.ip_bans" }}</span>
</div>
<button onclick="prompt_ban_ip()" class="quaternary small">
{{ icon "plus" }}
<span>{{ text "communities:action.create" }}</span>
</button>
</div>
<div class="card flex flex-col gap-2">
<!-- prettier-ignore -->
{% for item in items %}
<div class="card-nest">
<a
class="card small flex items-center gap-2 flush"
href="/api/v1/auth/user/find/{{ item.moderator }}"
>
<!-- prettier-ignore -->
{{ components::avatar(username=item.moderator, selector_type="id") }}
<span>{{ item.moderator }}</span>
<span class="fade date">{{ item.created }}</span>
</a>
<div class="card secondary flex flex-col gap-2">
<code>{{ item.ip }}</code>
<span>{{ item.reason|markdown|safe }}</span>
<div class="card w-full flex flex-wrap gap-2">
<button
onclick="remove_ipban('{{ item.ip }}')"
class="red quaternary"
>
{{ icon "trash" }}
<span>{{ text "general:action.delete" }}</span>
</button>
</div>
</div>
</div>
{% endfor %}
<!-- prettier-ignore -->
{{ components::pagination(page=page, items=items|length) }}
</div>
</div>
</main>
<script>
async function prompt_ban_ip() {
const ip = await trigger("atto::prompt", ["IP address (or prefix):"]);
if (!ip) {
return;
}
trigger("atto::ban_ip", [ip]);
}
async function remove_ipban(ip) {
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this?",
]))
) {
return;
}
fetch(`/api/v1/bans/${ip}`, {
method: "DELETE",
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,86 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "IP Bans - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(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 \"ban\" }}")
(span
(text "{{ text \"general:link.ip_bans\" }}")))
(button
("onclick" "prompt_ban_ip()")
("class" "quaternary small")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"communities:action.create\" }}"))))
(div
("class" "card flex flex-col gap-2")
(text "{% for item in items %}")
(div
("class" "card-nest")
(a
("class" "card small flex items-center gap-2 flush")
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
(text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}")
(span
(text "{{ item.moderator }}"))
(span
("class" "fade date")
(text "{{ item.created }}")))
(div
("class" "card secondary flex flex-col gap-2")
(code
(text "{{ item.ip }}"))
(span
(text "{{ item.reason|markdown|safe }}"))
(div
("class" "card w-full flex flex-wrap gap-2")
(button
("onclick" "remove_ipban('{{ item.ip }}')")
("class" "red quaternary")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"general:action.delete\" }}"))))))
(text "{% endfor %}")
(text "{{ components::pagination(page=page, items=items|length) }}"))))
(script
(text "async function prompt_ban_ip() {
const ip = await trigger(\"atto::prompt\", [\"IP address (or prefix):\"]);
if (!ip) {
return;
}
trigger(\"atto::ban_ip\", [ip]);
}
async function remove_ipban(ip) {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/bans/${ip}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}"))
(text "{% endblock %}")

View file

@ -1,253 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Manage profile - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div class="card-nest w-full">
<div class="card small flex items-center gap-2">
{{ icon "shield" }}
<span>{{ text "mod_panel:label.manage_profile" }}</span>
</div>
<div class="card tertiary">
<div class="flex flex-col gap-2" id="mod_options">
<div
class="card w-full flex flex-wrap gap-2"
ui_ident="actions"
>
<a
href="/settings?username={{ profile.username }}"
class="button quaternary"
>
{{ icon "settings" }}
<span>View settings</span>
</a>
<a
href="/mod_panel/profile/{{ profile.id }}/warnings"
class="button quaternary"
>
{{ icon "shield-alert" }}
<span>View warnings</span>
</a>
<button
class="red quaternary"
onclick="delete_account(event)"
>
{{ icon "trash" }}
<span>{{ text "settings:label.delete_account" }}</span>
</button>
{% if profile.permissions != 131073 -%}
<button
class="red quaternary"
onclick="update_user_role(131073)"
>
Ban
</button>
{% else %}
<button class="quaternary" onclick="update_user_role(1)">
Unban
</button>
{%- endif %}
</div>
</div>
<script>
setTimeout(() => {
const ui = ns("ui");
const element = document.getElementById("mod_options");
async function profile_request(do_confirm, path, body) {
if (do_confirm) {
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this?",
]))
) {
return;
}
}
fetch(`/api/v1/auth/user/{{ profile.id }}/${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
}
globalThis.delete_account = async (e) => {
e.preventDefault();
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this?",
]))
) {
return;
}
fetch("/api/v1/auth/user/{{ profile.id }}", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
password: "",
}),
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
};
globalThis.update_user_role = async (new_role) => {
if (
!(await trigger("atto::confirm", [
"Are you sure you would like to do this?",
]))
) {
return;
}
fetch(`/api/v1/auth/user/{{ profile.id }}/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,
]);
});
};
ui.refresh_container(element, ["actions"]);
setTimeout(() => {
ui.refresh_container(element, ["actions"]);
ui.generate_settings_ui(
element,
[
[
["is_verified", "Is verified"],
"{{ profile.is_verified }}",
"checkbox",
],
[
["role", "Permission level"],
"{{ profile.permissions }}",
"input",
],
],
null,
{
is_verified: (value) => {
profile_request(false, "verified", {
is_verified: value,
});
},
role: (new_role) => {
return update_user_role(new_role);
},
},
);
}, 100);
}, 150);
</script>
</div>
</div>
<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 "blocks" }}
<span
>{{ text "mod_panel:label.permissions_level_builder"
}}</span
>
</div>
<button
class="small quaternary"
onclick="update_user_role(Number.parseInt(document.getElementById('role').value))"
>
{{ icon "check" }}
<span>{{ text "general:action.save" }}</span>
</button>
</div>
<div
class="card tertiary flex flex-col gap-2"
id="permission_builder"
></div>
</div>
<script>
setTimeout(() => {
const get_permissions_html = trigger(
"ui::generate_permissions_ui",
[
{
// https://trisuaso.github.io/tetratto/tetratto/model/permissions/struct.FinePermission.html
DEFAULT: 1 << 0,
ADMINISTRATOR: 1 << 1,
MANAGE_COMMUNITIES: 1 << 2,
MANAGE_POSTS: 1 << 3,
MANAGE_POST_REPLIES: 1 << 4,
MANAGE_USERS: 1 << 5,
MANAGE_BANS: 1 << 6,
MANAGE_WARNINGS: 1 << 7,
MANAGE_NOTIFICATIONS: 1 << 8,
VIEW_REPORTS: 1 << 9,
VIEW_AUDIT_LOG: 1 << 10,
MANAGE_MEMBERSHIPS: 1 << 11,
MANAGE_REACTIONS: 1 << 12,
MANAGE_FOLLOWS: 1 << 13,
MANAGE_VERIFIED: 1 << 14,
MANAGE_AUDITLOG: 1 << 15,
MANAGE_REPORTS: 1 << 16,
BANNED: 1 << 17,
INFINITE_COMMUNITIES: 1 << 18,
SUPPORTER: 1 << 19,
MANAGE_REQUESTS: 1 << 20,
MANAGE_QUESTIONS: 1 << 21,
MANAGE_CHANNELS: 1 << 22,
MANAGE_MESSAGES: 1 << 23,
MANAGE_UPLOADS: 1 << 24,
MANAGE_EMOJIS: 1 << 25,
MANAGE_STACKS: 1 << 26,
STAFF_BADGE: 1 << 27,
},
],
);
document.getElementById("permission_builder").innerHTML =
get_permissions_html(
Number.parseInt("{{ profile.permissions }}"),
"permission_builder",
);
}, 250);
</script>
</main>
{% endblock %}

View file

@ -0,0 +1,237 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Manage profile - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"shield\" }}")
(span
(text "{{ text \"mod_panel:label.manage_profile\" }}")))
(div
("class" "card tertiary")
(div
("class" "flex flex-col gap-2")
("id" "mod_options")
(div
("class" "card w-full flex flex-wrap gap-2")
("ui_ident" "actions")
(a
("href" "/settings?username={{ profile.username }}")
("class" "button quaternary")
(text "{{ icon \"settings\" }}")
(span
(text "View settings")))
(a
("href" "/mod_panel/profile/{{ profile.id }}/warnings")
("class" "button quaternary")
(text "{{ icon \"shield-alert\" }}")
(span
(text "View warnings")))
(button
("class" "red quaternary")
("onclick" "delete_account(event)")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"settings:label.delete_account\" }}")))
(text "{% if profile.permissions != 131073 -%}")
(button
("class" "red quaternary")
("onclick" "update_user_role(131073)")
(text "Ban"))
(text "{% else %}")
(button
("class" "quaternary")
("onclick" "update_user_role(1)")
(text "Unban"))
(text "{%- endif %}")))
(script
(text "setTimeout(() => {
const ui = ns(\"ui\");
const element = document.getElementById(\"mod_options\");
async function profile_request(do_confirm, path, body) {
if (do_confirm) {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
}
fetch(`/api/v1/auth/user/{{ profile.id }}/${path}`, {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify(body),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}
globalThis.delete_account = async (e) => {
e.preventDefault();
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(\"/api/v1/auth/user/{{ profile.id }}\", {
method: \"DELETE\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
password: \"\",
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};
globalThis.update_user_role = async (new_role) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/auth/user/{{ profile.id }}/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,
]);
});
};
ui.refresh_container(element, [\"actions\"]);
setTimeout(() => {
ui.refresh_container(element, [\"actions\"]);
ui.generate_settings_ui(
element,
[
[
[\"is_verified\", \"Is verified\"],
\"{{ profile.is_verified }}\",
\"checkbox\",
],
[
[\"role\", \"Permission level\"],
\"{{ profile.permissions }}\",
\"input\",
],
],
null,
{
is_verified: (value) => {
profile_request(false, \"verified\", {
is_verified: value,
});
},
role: (new_role) => {
return update_user_role(new_role);
},
},
);
}, 100);
}, 150);"))))
(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 \"blocks\" }}")
(span
(text "{{ text \"mod_panel:label.permissions_level_builder\" }}")))
(button
("class" "small quaternary")
("onclick" "update_user_role(Number.parseInt(document.getElementById('role').value))")
(text "{{ icon \"check\" }}")
(span
(text "{{ text \"general:action.save\" }}"))))
(div
("class" "card tertiary flex flex-col gap-2")
("id" "permission_builder")))
(script
(text "setTimeout(() => {
const get_permissions_html = trigger(
\"ui::generate_permissions_ui\",
[
{
// https://trisuaso.github.io/tetratto/tetratto/model/permissions/struct.FinePermission.html
DEFAULT: 1 << 0,
ADMINISTRATOR: 1 << 1,
MANAGE_COMMUNITIES: 1 << 2,
MANAGE_POSTS: 1 << 3,
MANAGE_POST_REPLIES: 1 << 4,
MANAGE_USERS: 1 << 5,
MANAGE_BANS: 1 << 6,
MANAGE_WARNINGS: 1 << 7,
MANAGE_NOTIFICATIONS: 1 << 8,
VIEW_REPORTS: 1 << 9,
VIEW_AUDIT_LOG: 1 << 10,
MANAGE_MEMBERSHIPS: 1 << 11,
MANAGE_REACTIONS: 1 << 12,
MANAGE_FOLLOWS: 1 << 13,
MANAGE_VERIFIED: 1 << 14,
MANAGE_AUDITLOG: 1 << 15,
MANAGE_REPORTS: 1 << 16,
BANNED: 1 << 17,
INFINITE_COMMUNITIES: 1 << 18,
SUPPORTER: 1 << 19,
MANAGE_REQUESTS: 1 << 20,
MANAGE_QUESTIONS: 1 << 21,
MANAGE_CHANNELS: 1 << 22,
MANAGE_MESSAGES: 1 << 23,
MANAGE_UPLOADS: 1 << 24,
MANAGE_EMOJIS: 1 << 25,
MANAGE_STACKS: 1 << 26,
STAFF_BADGE: 1 << 27,
},
],
);
document.getElementById(\"permission_builder\").innerHTML =
get_permissions_html(
Number.parseInt(\"{{ profile.permissions }}\"),
\"permission_builder\",
);
}, 250);")))
(text "{% endblock %}")

View file

@ -1,81 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Reports - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div class="card-nest w-full">
<div class="card small flex items-center gap-2">
{{ icon "flag" }}
<span>{{ text "general:link.reports" }}</span>
</div>
<div class="card flex flex-col gap-2">
<!-- prettier-ignore -->
{% for item in items %}
<div class="card-nest">
<a
class="card small flex items-center gap-2 flush"
href="/api/v1/auth/user/find/{{ item.owner }}"
>
<!-- prettier-ignore -->
{{ components::avatar(username=item.owner, selector_type="id") }}
<span>{{ item.owner }}</span>
<span class="fade date">{{ item.created }}</span>
</a>
<div class="card secondary flex flex-col gap-2">
<span class="no_p_margin"
>{{ item.content|markdown|safe }}</span
>
<div class="card w-full flex flex-wrap gap-2">
<button
onclick="open_reported_content('{{ item.asset }}', '{{ item.asset_type }}')"
>
{{ icon "external-link" }}
<span
>{{ text "mod_panel:label.open_reported_content"
}}</span
>
</button>
<button
onclick="remove_report('{{ item.id }}')"
class="red quaternary"
>
{{ icon "trash" }}
<span>{{ text "general:action.delete" }}</span>
</button>
</div>
</div>
</div>
{% endfor %}
<!-- prettier-ignore -->
{{ components::pagination(page=page, items=items|length) }}
</div>
</div>
</main>
<script>
function open_reported_content(asset, asset_type) {
if (asset_type === "Post") {
window.open(`/post/${asset}`);
} else if (asset_type === "Community") {
window.open(`/community/${asset}`);
}
}
function remove_report(id) {
fetch(`/api/v1/reports/${id}`, {
method: "DELETE",
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,72 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Reports - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"flag\" }}")
(span
(text "{{ text \"general:link.reports\" }}")))
(div
("class" "card flex flex-col gap-2")
(text "{% for item in items %}")
(div
("class" "card-nest")
(a
("class" "card small flex items-center gap-2 flush")
("href" "/api/v1/auth/user/find/{{ item.owner }}")
(text "{{ components::avatar(username=item.owner, selector_type=\"id\") }}")
(span
(text "{{ item.owner }}"))
(span
("class" "fade date")
(text "{{ item.created }}")))
(div
("class" "card secondary flex flex-col gap-2")
(span
("class" "no_p_margin")
(text "{{ item.content|markdown|safe }}"))
(div
("class" "card w-full flex flex-wrap gap-2")
(button
("onclick" "open_reported_content('{{ item.asset }}', '{{ item.asset_type }}')")
(text "{{ icon \"external-link\" }}")
(span
(text "{{ text \"mod_panel:label.open_reported_content\" }}")))
(button
("onclick" "remove_report('{{ item.id }}')")
("class" "red quaternary")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"general:action.delete\" }}"))))))
(text "{% endfor %}")
(text "{{ components::pagination(page=page, items=items|length) }}"))))
(script
(text "function open_reported_content(asset, asset_type) {
if (asset_type === \"Post\") {
window.open(`/post/${asset}`);
} else if (asset_type === \"Community\") {
window.open(`/community/${asset}`);
}
}
function remove_report(id) {
fetch(`/api/v1/reports/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}"))
(text "{% endblock %}")

View file

@ -1,31 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>Server stats - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div class="card-nest w-full">
<div class="card small flex items-center gap-2">
{{ icon "chart-line" }}
<span>{{ text "general:link.stats" }}</span>
</div>
<div class="card flex flex-col gap-2">
<ul>
<li>
<b>Active user streams:</b>
<span>{{ active_users }}</span>
</li>
<li>
<b>Active chat subscriptions:</b>
<span>{{ active_users_chats }}</span>
</li>
<li>
<b>Socket tasks:</b>
<span>{{ (active_users_chats + active_users) * 3 }}</span>
</li>
</ul>
</div>
</div>
</main>
{% endblock %}

View file

@ -0,0 +1,34 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Server stats - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "card-nest w-full")
(div
("class" "card small flex items-center gap-2")
(text "{{ icon \"chart-line\" }}")
(span
(text "{{ text \"general:link.stats\" }}")))
(div
("class" "card flex flex-col gap-2")
(ul
(li
(b
(text "Active user streams:"))
(span
(text "{{ active_users }}")))
(li
(b
(text "Active chat subscriptions:"))
(span
(text "{{ active_users_chats }}")))
(li
(b
(text "Socket tasks:"))
(span
(text "{{ (active_users_chats + active_users) * 3 }}")))))))
(text "{% endblock %}")

View file

@ -1,132 +0,0 @@
{% extends "root.html" %} {% block head %}
<title>User warnings - {{ config.name }}</title>
{% endblock %} {% block body %} {{ macros::nav() }}
<main class="flex flex-col gap-2">
<div class="card-nest">
<div class="card small flex items-center justify-between gap-2">
<span class="flex items-center gap-2">
{{ icon "gavel" }}
<span>{{ text "mod_panel:label.create_warning" }}</span>
</span>
<a
href="/mod_panel/profile/{{ profile.id }}"
class="button quaternary small red"
>
{{ icon "x" }}
<span>{{ text "dialog:action.cancel" }}</span>
</a>
</div>
<form
class="card flex flex-col gap-2"
onsubmit="create_warning_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"
required
minlength="2"
maxlength="4096"
></textarea>
</div>
<button class="primary">
{{ text "communities:action.create" }}
</button>
</form>
</div>
<div class="card-nest">
<div class="card small flex items-center justify-between gap-2">
<span class="flex items-center gap-2">
{{ icon "message-circle-warning" }}
<span>{{ text "mod_panel:label.warnings" }}</span>
</span>
</div>
<div class="card flex flex-col gap-4">
{% for item in items %}
<div class="card-nest">
<div class="card small flex items-center justify-between gap-2">
<a
class="flex items-center gap-2 flush"
href="/api/v1/auth/user/find/{{ item.moderator }}"
title="Moderator"
>
<!-- prettier-ignore -->
{{ components::avatar(username=item.moderator, selector_type="id") }}
<span>{{ item.moderator }}</span>
<span class="fade date">{{ item.created }}</span>
</a>
<button
class="small quaternary red"
onclick="remove_warning('{{ item.id }}')"
>
{{ icon "trash" }}
<span>{{ text "general:action.delete" }}</span>
</button>
</div>
<div class="card secondary flex flex-col gap-2">
<span class="no_p_margin"
>{{ item.content|markdown|safe }}</span
>
</div>
</div>
{% endfor %}
<!-- prettier-ignore -->
{{ components::pagination(page=page, items=items|length) }}
</div>
</div>
</main>
<script>
async function create_warning_from_form(e) {
e.preventDefault();
await trigger("atto::debounce", ["warnings::create"]);
fetch("/api/v1/warnings/{{ profile.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.reset();
}
});
}
function remove_warning(id) {
fetch(`/api/v1/warnings/${id}`, {
method: "DELETE",
})
.then((res) => res.json())
.then((res) => {
trigger("atto::toast", [
res.ok ? "success" : "error",
res.message,
]);
});
}
</script>
{% endblock %}

View file

@ -0,0 +1,121 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "User warnings - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "card-nest")
(div
("class" "card small flex items-center justify-between gap-2")
(span
("class" "flex items-center gap-2")
(text "{{ icon \"gavel\" }}")
(span
(text "{{ text \"mod_panel:label.create_warning\" }}")))
(a
("href" "/mod_panel/profile/{{ profile.id }}")
("class" "button quaternary small red")
(text "{{ icon \"x\" }}")
(span
(text "{{ text \"dialog:action.cancel\" }}"))))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "create_warning_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")
("required" "")
("minlength" "2")
("maxlength" "4096")))
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))
(div
("class" "card-nest")
(div
("class" "card small flex items-center justify-between gap-2")
(span
("class" "flex items-center gap-2")
(text "{{ icon \"message-circle-warning\" }}")
(span
(text "{{ text \"mod_panel:label.warnings\" }}"))))
(div
("class" "card flex flex-col gap-4")
(text "{% for item in items %}")
(div
("class" "card-nest")
(div
("class" "card small flex items-center justify-between gap-2")
(a
("class" "flex items-center gap-2 flush")
("href" "/api/v1/auth/user/find/{{ item.moderator }}")
("title" "Moderator")
(text "{{ components::avatar(username=item.moderator, selector_type=\"id\") }}")
(span
(text "{{ item.moderator }}"))
(span
("class" "fade date")
(text "{{ item.created }}")))
(button
("class" "small quaternary red")
("onclick" "remove_warning('{{ item.id }}')")
(text "{{ icon \"trash\" }}")
(span
(text "{{ text \"general:action.delete\" }}"))))
(div
("class" "card secondary flex flex-col gap-2")
(span
("class" "no_p_margin")
(text "{{ item.content|markdown|safe }}"))))
(text "{% endfor %}")
(text "{{ components::pagination(page=page, items=items|length) }}"))))
(script
(text "async function create_warning_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"warnings::create\"]);
fetch(\"/api/v1/warnings/{{ profile.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.reset();
}
});
}
function remove_warning(id) {
fetch(`/api/v1/warnings/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}"))
(text "{% endblock %}")