tetratto/crates/app/src/public/html/littleweb/domain.lisp

279 lines
10 KiB
Common Lisp
Raw Normal View History

2025-07-08 13:35:23 -04:00
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "My services - {{ config.name }}"))
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2")
2025-07-08 13:35:23 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card_nest")
2025-07-08 13:35:23 -04:00
(div
("class" "card small")
(b
(text "{{ domain.name }}.{{ domain.tld|lower }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "flex flex_col gap_2 card")
2025-07-08 13:35:23 -04:00
(code
2025-08-03 23:24:57 -04:00
("class" "w_content")
2025-07-08 13:35:23 -04:00
(a
("href" "atto://{{ domain.name }}.{{ domain.tld|lower }}")
(text "atto://{{ domain.name }}.{{ domain.tld|lower }}")))
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 flex_wrap")
2025-07-08 13:35:23 -04:00
(button
("class" "red lowered")
("onclick" "delete_domain()")
(icon (text "trash"))
(str (text "general:action.delete"))))))
(text "{%- endif %}")
(div
2025-08-03 23:24:57 -04:00
("class" "card_nest w_full")
2025-07-08 13:35:23 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "card small flex flex_col gap_2")
2025-07-08 13:35:23 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center justify_between gap_2")
2025-07-08 13:35:23 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex items_center gap_2")
2025-07-08 13:35:23 -04:00
(icon (text "panel-top"))
(span
(str (text "littleweb:label.domain_data"))))
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2")
2025-07-08 13:35:23 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "card w_full lowered flex flex_col gap_2 hidden no_p_margin")
2025-07-08 13:35:23 -04:00
("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
2025-08-03 23:24:57 -04:00
("class" "card flex flex_col gap_2")
2025-07-08 13:35:23 -04:00
; add data
(form
("id" "add_data")
2025-08-03 23:24:57 -04:00
("class" "card hidden w_full lowered flex flex_col gap_2")
2025-07-08 13:35:23 -04:00
("onsubmit" "add_data_from_form(event)")
(div
2025-08-03 23:24:57 -04:00
("class" "flex gap_2 flex_collapse")
2025-07-08 13:35:23 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "flex w_full flex_col gap_1")
2025-07-08 13:35:23 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex w_full flex_col gap_1")
2025-07-08 13:35:23 -04:00
(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
2025-08-03 23:24:57 -04:00
("class" "flex w_full flex_col gap_1")
2025-07-08 13:35:23 -04:00
(label
("for" "value")
(str (text "littleweb:label.value")))
(input
("type" "text")
("name" "value")
("id" "value")
("placeholder" "value")
("required" "")
("minlength" "2")
("maxlength" "256"))))
(div
2025-08-03 23:24:57 -04:00
("class" "flex w_full justify_between")
2025-07-08 13:35:23 -04:00
(div)
(button
(icon (text "check"))
(str (text "general:action.save")))))
; data
2025-07-08 15:21:57 -04:00
(div
2025-08-03 23:24:57 -04:00
("class" "w_full")
("style" "max-width: 100%; overflow: auto; min-height: 512px")
2025-07-08 15:21:57 -04:00
(table
2025-08-03 23:24:57 -04:00
("class" "w_full")
2025-07-08 15:21:57 -04:00
(thead
(tr
(th (text "Name"))
(th (text "Type"))
(th (text "Value"))
(th (text "Actions"))))
2025-07-08 13:35:23 -04:00
2025-07-08 15:21:57 -04:00
(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")
2025-07-08 13:35:23 -04:00
(div
2025-07-08 15:21:57 -04:00
("class" "dropdown")
2025-07-08 13:35:23 -04:00
(button
2025-07-08 15:21:57 -04:00
("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")))
2025-07-08 13:35:23 -04:00
2025-07-08 15:21:57 -04:00
(button
("class" "red")
("onclick" "remove_data('{{ item[0] }}')")
(icon (text "trash"))
(str (text "general:action.delete")))))))
(text "{%- endfor %}")))))))
2025-07-08 13:35:23 -04:00
(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 %}")