add: mail ui

This commit is contained in:
trisua 2025-08-02 16:04:50 -04:00
parent 2e60cbc464
commit b2a73d286b
24 changed files with 993 additions and 259 deletions

View file

@ -0,0 +1,139 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Compose letter - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
(div
("class" "card-nest")
(div
("class" "card small items-center gap-2 flex justify-between")
(div
("class" "flex gap-2 items-center")
(icon (text "mail-plus"))
(str (text "mail:label.compose")))
(button
("onclick" "window.history.back()")
("class" "lowered small")
(icon (text "arrow-left"))
(str (text "general:action.back"))))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "create_letter_from_form(event)")
(div
("class" "flex flex-col gap-1")
(span
(b (str (text "mail:label.receivers"))))
(div
("class" "flex flex-wrap gap-2 small card lowered")
(div ("id" "receivers") ("class" "flex flex-wrap gap-2"))
(button
("class" "small tiny big_icon square raised")
("onclick" "add_receiver()")
("type" "button")
(icon (text "plus")))))
(div
("class" "flex flex-col gap-1")
(label
("for" "subject")
(b (str (text "mail:label.subject"))))
(input
("type" "text")
("placeholder" "subject")
("required" "")
("name" "subject")
("id" "subject")))
(div
("class" "flex flex-col gap-1")
(label
("for" "content")
(b (str (text "mail:label.content"))))
(textarea
("placeholder" "content")
("required" "")
("name" "content")
("id" "content")))
(button
(icon (text "send-horizontal"))
(str (text "mail:action.send"))))))
(script
(text "globalThis.RECEIVERS = [];
globalThis.SEARCH_PARAMS = new URLSearchParams(window.location.search);
globalThis.add_receiver = async () => {
const username = await trigger(\"atto::prompt\", [\"Username:\"]);
if (!username) {
return;
}
RECEIVERS.push(username);
render_receivers();
}
globalThis.remove_receiver = (username) => {
RECEIVERS.splice(RECEIVERS.indexOf(username), 1);
render_receivers();
}
globalThis.render_receivers = () => {
const element = document.getElementById(\"receivers\");
element.innerHTML = \"\";
for (let receiver of RECEIVERS) {
const is_id = receiver.startsWith(\"id:\");
receiver = receiver.replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replace(\"id:\", \"\");
element.innerHTML += `<button class=\"small lowered\" onclick=\"remove_receiver('${receiver}')\" type=\"button\">
<img class=\"avatar\" style=\"--size: 18px\" src=\"/api/v1/auth/user/${receiver}/avatar?selector_type=${is_id ? \"id\" : \"username\"}\" />
<span>${is_id ? \"...\" : receiver}</span>
</button>`;
}
}
globalThis.create_letter_from_form = async (e) => {
e.preventDefault();
await trigger(\"atto::debounce\", [\"letters::create\"]);
fetch(\"/api/v1/letters\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
content: e.target.content.value.trim(),
subject: e.target.subject.value.trim(),
receivers: RECEIVERS,
replying_to: SEARCH_PARAMS.get(\"replying_to\") || \"0\",
}),
})
.then((res) => res.json())
.then((res) => {
if (!res.ok) {
trigger(\"atto::toast\", [\"error\", res.message]);
} else {
e.target.reset();
window.location.href = `/mail/letter/${res.payload}`;
}
});
};
if (SEARCH_PARAMS.get(\"receivers\")) {
let r = SEARCH_PARAMS.get(\"receivers\");
if (r.startsWith(\",\")) {
r = r.replace(\",\", \"\");
}
RECEIVERS = r.split(\",\");
render_receivers();
}
if (SEARCH_PARAMS.get(\"subject\")) {
document.getElementById(\"subject\").value = SEARCH_PARAMS.get(\"subject\");
}"))
(text "{% endblock %}")

View file

@ -0,0 +1,49 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Letter - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(text "{{ components::letter(letter=letter, owner=owner) }}")
(text "{% for letter in replies %}")
(text "{{ components::letter(letter=letter[1], owner=letter[0], show_subject=false) }}")
(text "{%- endfor %}")
(text "{{ components::pagination(page=page, items=replies|length) }}"))
(script
(text "globalThis.delete_letter = async (id) => {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(`/api/v1/letters/${id}`, {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}
fetch(\"/api/v1/letters/{{ letter.id }}/read\", {
method: \"POST\",
})
.then((res) => res.json())
.then((res) => {
if (!res.ok) {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
}
});"))
(text "{% endblock %}")

View file

@ -0,0 +1,43 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Received mail - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "pillmenu")
(a
("href" "/mail")
("class" "active")
(str (text "mail:label.received")))
(a
("href" "/mail/sent")
(str (text "mail:label.sent"))))
; letters
(div
("class" "card-nest")
(div
("class" "card small flex items-center justify-between gap-2")
(div
("class" "flex items-center gap-2")
(icon (text "mailbox"))
(str (text "mail:label.received")))
(a
("href" "/mail/compose")
("class" "button small lowered")
(icon (text "plus"))
(str (text "mail:label.compose"))))
(div
("class" "card flex flex-col gap-2")
(text "{% for letter in list %}")
(text "{{ components::letter_listing(letter=letter[1], owner=letter[0]) }}")
(text "{% endfor %}")
; pagination
(text "{% if list|length == 0 -%}")
(i ("class" "fade") (text "Nothing yet!"))
(text "{% else %}")
(text "{{ components::pagination(page=page, items=list|length) }}")
(text "{%- endif %}"))))
(text "{% endblock %}")

View file

@ -0,0 +1,43 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Sent mail - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex-col gap-2")
(div
("class" "pillmenu")
(a
("href" "/mail")
(str (text "mail:label.received")))
(a
("href" "/mail/sent")
("class" "active")
(str (text "mail:label.sent"))))
; letters
(div
("class" "card-nest")
(div
("class" "card small flex items-center justify-between gap-2")
(div
("class" "flex items-center gap-2")
(icon (text "mailbox"))
(str (text "mail:label.sent")))
(a
("href" "/mail/compose")
("class" "button small lowered")
(icon (text "plus"))
(str (text "mail:label.compose"))))
(div
("class" "card flex flex-col gap-2")
(text "{% for letter in list %}")
(text "{{ components::letter_listing(letter=letter[1], owner=letter[0]) }}")
(text "{% endfor %}")
; pagination
(text "{% if list|length == 0 -%}")
(i ("class" "fade") (text "Nothing yet!"))
(text "{% else %}")
(text "{{ components::pagination(page=page, items=list|length) }}")
(text "{%- endif %}"))))
(text "{% endblock %}")