tetratto/crates/app/src/public/html/littleweb/domain.lisp
2025-08-03 23:24:57 -04:00

278 lines
10 KiB
Common Lisp

(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "My services - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex_col gap_2")
(text "{% if user -%}")
(div
("class" "pillmenu")
(a ("href" "/services") (str (text "littleweb:label.services")))
(a ("href" "/domains") ("class" "active") (str (text "littleweb:label.domains"))))
(div
("class" "card_nest")
(div
("class" "card small")
(b
(text "{{ domain.name }}.{{ domain.tld|lower }}")))
(div
("class" "flex flex_col gap_2 card")
(code
("class" "w_content")
(a
("href" "atto://{{ domain.name }}.{{ domain.tld|lower }}")
(text "atto://{{ domain.name }}.{{ domain.tld|lower }}")))
(div
("class" "flex gap_2 flex_wrap")
(button
("class" "red lowered")
("onclick" "delete_domain()")
(icon (text "trash"))
(str (text "general:action.delete"))))))
(text "{%- endif %}")
(div
("class" "card_nest w_full")
(div
("class" "card small flex flex_col gap_2")
(div
("class" "flex items_center justify_between gap_2")
(div
("class" "flex items_center gap_2")
(icon (text "panel-top"))
(span
(str (text "littleweb:label.domain_data"))))
(div
("class" "flex gap_2")
(button
("class" "small lowered")
("title" "Help")
("onclick" "document.getElementById('domain_help').classList.toggle('hidden')")
(icon (text "circle-question-mark")))
(button
("class" "small")
("onclick" "document.getElementById('add_data').classList.toggle('hidden')")
(icon (text "plus"))
(str (text "littleweb:action.add")))))
(div
("class" "card w_full lowered flex flex_col gap_2 hidden no_p_margin")
("id" "domain_help")
(p (text "To link your domain to a site, go to the site and press \"Copy ID\"."))
(p (text "After you have the site's ID, click \"Add\" on this page, then paste the ID into the \"value\" field."))
(p (text "If you've ever managed a real domain's DNS, this should be familiar."))))
(div
("class" "card flex flex_col gap_2")
; add data
(form
("id" "add_data")
("class" "card hidden w_full lowered flex flex_col gap_2")
("onsubmit" "add_data_from_form(event)")
(div
("class" "flex gap_2 flex_collapse")
(div
("class" "flex w_full flex_col gap_1")
(label
("for" "name")
(str (text "littleweb:label.type")))
(select
("type" "text")
("name" "type")
("id" "type")
("placeholder" "type")
("required" "")
(option ("value" "Service") (text "Site ID"))
(option ("value" "Text") (text "Text"))))
(div
("class" "flex w_full flex_col gap_1")
(label
("for" "name")
(str (text "littleweb:label.name")))
(input
("type" "text")
("name" "name")
("id" "name")
("placeholder" "name")
("minlength" "1")
("maxlength" "32"))
(span ("class" "fade") (text "Use \"@\" for root.")))
(div
("class" "flex w_full flex_col gap_1")
(label
("for" "value")
(str (text "littleweb:label.value")))
(input
("type" "text")
("name" "value")
("id" "value")
("placeholder" "value")
("required" "")
("minlength" "2")
("maxlength" "256"))))
(div
("class" "flex w_full justify_between")
(div)
(button
(icon (text "check"))
(str (text "general:action.save")))))
; data
(div
("class" "w_full")
("style" "max-width: 100%; overflow: auto; min-height: 512px")
(table
("class" "w_full")
(thead
(tr
(th (text "Name"))
(th (text "Type"))
(th (text "Value"))
(th (text "Actions"))))
(tbody
(text "{% for item in domain.data -%}")
(tr
(td (text "{{ item[0] }}"))
(text "{% for k,v in item[1] -%}")
(td (text "{{ k }}"))
(td (text "{{ v }}"))
(text "{%- endfor %}")
(td
("style" "overflow: auto")
(div
("class" "dropdown")
(button
("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
(icon (text "ellipsis")))
(div
("class" "inner")
(button
("onclick" "rename_data('{{ item[0] }}')")
(icon (text "pencil"))
(str (text "littleweb:action.rename")))
(button
("class" "red")
("onclick" "remove_data('{{ item[0] }}')")
(icon (text "trash"))
(str (text "general:action.delete")))))))
(text "{%- endfor %}")))))))
(script ("id" "domain_data") ("type" "application/json") (text "{{ domain.data|json_encode()|safe }}"))
(script
(text "globalThis.DOMAIN_DATA = JSON.parse(document.getElementById(\"domain_data\").innerText);
async function save_data() {
await trigger(\"atto::debounce\", [\"domains::update_data\"]);
fetch(\"/api/v1/domains/{{ domain.id }}/data\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
data: DOMAIN_DATA,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}
async function add_data_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"domains::add_data\"]);
const x = {};
x[e.target.type.selectedOptions[0].value] = e.target.value.value;
if (e.target.name.value === \"\") {
e.target.name.value = \"@\";
}
const name = e.target.name.value.replace(\" \", \"_\");
if (DOMAIN_DATA.find((x) => x[0] === name)) {
return;
}
DOMAIN_DATA.push([name, x]);
await save_data();
e.target.reset();
}
async function delete_data(name) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"domains::delete_data\"]);
delete DOMAIN_DATA.find((x) => x[0] === name);
await save_data();
}
async function delete_domain() {
await trigger(\"atto::debounce\", [\"domains::delete\"]);
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(\"/api/v1/domains/{{ domain.id }}\", {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}
async function rename_data(selector) {
await trigger(\"atto::debounce\", [\"domains::rename_data\"]);
let name = await trigger(\"atto::prompt\", [\"New name:\"]);
if (!name) {
return;
}
DOMAIN_DATA.find((x) => x[0] === selector)[0] = name.replaceAll(\" \", \"_\");
await save_data();
setTimeout(() => {
window.location.reload();
}, 150);
}
async function remove_data(name) {
await trigger(\"atto::debounce\", [\"domains::remove_data\"]);
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
let i = 0;
DOMAIN_DATA.find((x) => {
i += 1;
return x[0] === name;
});
DOMAIN_DATA.splice(i - 1, 1);
await save_data();
}"))
(text "{% endblock %}")