568 lines
20 KiB
Common Lisp
568 lines
20 KiB
Common Lisp
(text "{% extends \"root.html\" %} {% block head %}")
|
|
(title
|
|
(text "Manage product - {{ config.name }}"))
|
|
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
|
|
(main
|
|
("class" "flex flex_col gap_2")
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex items_center gap_2")
|
|
(icon (text "images"))
|
|
(b
|
|
(str (text "economy:label.thumbnails"))))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(text "{{ components::post_media(upload_ids=product.uploads.thumbnails, custom_click=\"remove_thumbnail(event.target)\") }}")
|
|
(text "{% if product.uploads.thumbnails|length < 4 -%}")
|
|
(button
|
|
("onclick" "add_thumbnail()")
|
|
(icon (text "plus"))
|
|
(str (text "communities:label.upload")))
|
|
(text "{%- endif %}")))
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex gap_2 items_center")
|
|
(icon (text "pencil-line"))
|
|
(b
|
|
(str (text "economy:label.title"))))
|
|
(form
|
|
("class" "card flex flex_col gap_2")
|
|
("onsubmit" "update_title_from_form(event)")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "title")
|
|
(str (text "economy:label.title")))
|
|
(input
|
|
("type" "text")
|
|
("name" "title")
|
|
("id" "title")
|
|
("placeholder" "title")
|
|
("required" "")
|
|
("minlength" "2")
|
|
("maxlength" "128")
|
|
("value" "{{ product.title }}")))
|
|
(button (str (text "general:action.save")))))
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex gap_2 items_center")
|
|
(icon (text "pencil-line"))
|
|
(b
|
|
(str (text "economy:label.description"))))
|
|
(form
|
|
("class" "card flex flex_col gap_2")
|
|
("onsubmit" "update_description_from_form(event)")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "description")
|
|
(str (text "economy:label.description")))
|
|
(textarea
|
|
("name" "description")
|
|
("id" "description")
|
|
("placeholder" "description")
|
|
("required" "")
|
|
("minlength" "2")
|
|
("maxlength" "1024")
|
|
(text "{{ product.description }}")))
|
|
(button (str (text "general:action.save")))))
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex gap_2 items_center")
|
|
(icon (text "badge-cent"))
|
|
(b
|
|
(str (text "economy:label.price"))))
|
|
(form
|
|
("class" "card flex flex_col gap_2")
|
|
("onsubmit" "update_price_from_form(event)")
|
|
(label
|
|
("for" "on_sale")
|
|
("class" "flex items_center gap_2")
|
|
(input
|
|
("type" "checkbox")
|
|
("id" "on_sale")
|
|
("name" "on_sale")
|
|
("class" "w_content")
|
|
("checked" "{{ product.on_sale }}")
|
|
("oninput" "event.preventDefault(); update_on_sale_from_form(event.target.checked)"))
|
|
(span
|
|
(str (text "economy:label.on_sale"))))
|
|
(label
|
|
("for" "single_use")
|
|
("class" "flex items_center gap_2")
|
|
(input
|
|
("type" "checkbox")
|
|
("id" "single_use")
|
|
("name" "single_use")
|
|
("class" "w_content")
|
|
("checked" "{{ product.single_use }}")
|
|
("oninput" "event.preventDefault(); update_single_use_from_form(event.target.checked)"))
|
|
(span
|
|
(str (text "economy:label.single_use"))))
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "price")
|
|
(str (text "economy:label.price")))
|
|
(input
|
|
("type" "number")
|
|
("name" "price")
|
|
("id" "price")
|
|
("placeholder" "price")
|
|
("required" "")
|
|
("min" "0")
|
|
("max" "1000000")
|
|
("value" "{{ product.price }}")))
|
|
(button (str (text "general:action.save")))))
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex gap_2 items_center")
|
|
(icon (text "weight"))
|
|
(b
|
|
(str (text "economy:label.stock"))))
|
|
(form
|
|
("class" "card flex flex_col gap_2")
|
|
("onsubmit" "update_stock_from_form(event)")
|
|
(label
|
|
("for" "unlimited")
|
|
("class" "flex items_center gap_2")
|
|
(input
|
|
("type" "checkbox")
|
|
("id" "unlimited")
|
|
("name" "unlimited")
|
|
("class" "w_content")
|
|
("checked" "{{ product.stock == -1 }}")
|
|
("oninput" "event.preventDefault(); event.target.checked ? document.getElementById('stock').value = -1 : document.getElementById('stock').value = 0"))
|
|
(span
|
|
(str (text "economy:label.unlimited"))))
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "stock")
|
|
(str (text "economy:label.stock")))
|
|
(input
|
|
("type" "number")
|
|
("name" "stock")
|
|
("id" "stock")
|
|
("placeholder" "stock")
|
|
("required" "")
|
|
("min" "-1")
|
|
("max" "1000000")
|
|
("value" "{{ product.stock }}")))
|
|
(button (str (text "general:action.save")))))
|
|
|
|
(div
|
|
("class" "card_nest")
|
|
(div
|
|
("class" "card small flex gap_2 items_center")
|
|
(icon (text "package-check"))
|
|
(b
|
|
(str (text "economy:label.fulfillment_style"))))
|
|
(div
|
|
("class" "card flex flex_col gap_2")
|
|
(select
|
|
("id" "fulfillment_style_select")
|
|
("onchange" "mirror_fulfillment_style_select(true)")
|
|
(option ("value" "mail") (text "Mail") ("selected" "{{ not product.method == \"ProfileStyle\" }}"))
|
|
(option ("value" "snippet") (text "CSS Snippet") ("selected" "{{ product.method == \"ProfileStyle\" }}")))
|
|
(form
|
|
("class" "flex flex_col gap_2 hidden")
|
|
("id" "mail_fulfillment")
|
|
("onsubmit" "update_method_from_form(event)")
|
|
(p (text "If you choose to send an automated mail letter upon purchase, users will automatically receive the message you supply below."))
|
|
(p (text "If you disable automail, you'll be required to manually mail users who have purchased your product before the transfer is finalized."))
|
|
(text "{% set is_automail = product.method != \"ManualMail\" and product.method != \"ProfileStyle\" %}")
|
|
|
|
(label
|
|
("for" "use_automail")
|
|
("class" "flex items_center gap_2")
|
|
(input
|
|
("type" "checkbox")
|
|
("id" "use_automail")
|
|
("name" "use_automail")
|
|
("class" "w_content")
|
|
("oninput" "mirror_use_automail()")
|
|
("checked" "{% if is_automail -%} true {%- else -%} false {%- endif %}"))
|
|
(span
|
|
(str (text "economy:label.use_automail"))))
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "automail_message")
|
|
(str (text "economy:label.automail_message")))
|
|
(textarea
|
|
("name" "automail_message")
|
|
("id" "automail_message")
|
|
("placeholder" "automail_message")
|
|
(text "{% if is_automail -%} {{ product.method.AutoMail }} {%- endif %}")))
|
|
(button (str (text "general:action.save"))))
|
|
(form
|
|
("class" "flex flex_col gap_2 hidden")
|
|
("id" "snippet_fulfillment")
|
|
("onsubmit" "update_data_from_form(event)")
|
|
(text "{{ components::supporter_ad(body=\"Become a supporter to create snippets!\") }}")
|
|
(div
|
|
("class" "flex flex_col gap_1")
|
|
(label
|
|
("for" "data")
|
|
(str (text "economy:label.snippet_data")))
|
|
(textarea
|
|
("name" "data")
|
|
("id" "data")
|
|
("placeholder" "data")
|
|
(text "{{ product.data }}")))
|
|
(button (str (text "general:action.save"))))))
|
|
|
|
(div
|
|
("class" "flex gap_2")
|
|
(a
|
|
("class" "button secondary")
|
|
("href" "/product/{{ product.id }}")
|
|
(icon (text "arrow-left"))
|
|
(str (text "general:action.back")))
|
|
|
|
(button
|
|
("class" "lowered red")
|
|
("onclick" "delete_product()")
|
|
(icon (text "trash"))
|
|
(str (text "general:action.delete")))))
|
|
|
|
(script
|
|
(text "async function add_thumbnail() {
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
const picker = document.createElement(\"input\");
|
|
picker.type = \"file\";
|
|
picker.accept = \"image/*\";
|
|
document.body.appendChild(picker);
|
|
picker.click();
|
|
|
|
picker.addEventListener(\"change\", () => {
|
|
// create body
|
|
const body = new FormData();
|
|
|
|
for (const file of picker.files) {
|
|
body.append(file.name, file);
|
|
}
|
|
|
|
body.append(
|
|
\"body\",
|
|
JSON.stringify({
|
|
target: \"Thumbnails\"
|
|
}),
|
|
);
|
|
|
|
// ...
|
|
picker.remove();
|
|
fetch(\"/api/v1/products/{{ product.id }}/uploads\", {
|
|
method: \"POST\",
|
|
body,
|
|
})
|
|
.then((res) => res.json())
|
|
.then(async (res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 100);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function remove_thumbnail(target) {
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/uploads/thumbnails\", {
|
|
method: \"DELETE\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
idx: Array.from(target.parentElement.children).findIndex((x) => x.getAttribute(\"data-upload-id\") === target.getAttribute(\"data-upload-id\")),
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then(async (res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
|
|
if (res.ok) {
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 100);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function update_title_from_form(e) {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/title\", {
|
|
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,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function update_description_from_form(e) {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/description\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
description: e.target.description.value,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function update_on_sale_from_form(on_sale) {
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/on_sale\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
on_sale,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function update_single_use_from_form(single_use) {
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/single_use\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
single_use,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function update_price_from_form(e) {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/price\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
price: e.target.price.valueAsNumber,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function update_stock_from_form(e) {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/stock\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
stock: e.target.stock.valueAsNumber,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function update_method_from_form(e) {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/method\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
method: e.target.use_automail.checked ? { AutoMail: e.target.automail_message.value } : \"ManualMail\",
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function update_data_from_form(e) {
|
|
e.preventDefault();
|
|
await trigger(\"atto::debounce\", [\"products::update\"]);
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}/data\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
data: e.target.data.value,
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
|
|
async function delete_product() {
|
|
if (
|
|
!(await trigger(\"atto::confirm\", [
|
|
\"Are you sure you would like to do this?\",
|
|
]))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
fetch(\"/api/v1/products/{{ product.id }}\", {
|
|
method: \"DELETE\",
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
};
|
|
|
|
globalThis.mirror_use_automail = () => {
|
|
const use_automail = document.getElementById(\"use_automail\").checked;
|
|
|
|
if (use_automail) {
|
|
document.getElementById(\"automail_message\").removeAttribute(\"disabled\");
|
|
} else {
|
|
document.getElementById(\"automail_message\").setAttribute(\"disabled\", \"true\");
|
|
}
|
|
}
|
|
|
|
globalThis.mirror_fulfillment_style_select = (send = false) => {
|
|
const selected = document.getElementById(\"fulfillment_style_select\").selectedOptions[0].value;
|
|
|
|
if (selected === \"mail\") {
|
|
document.getElementById(\"mail_fulfillment\").classList.remove(\"hidden\");
|
|
document.getElementById(\"snippet_fulfillment\").classList.add(\"hidden\");
|
|
|
|
if (send) {
|
|
update_method_from_form({
|
|
preventDefault: () => {},
|
|
target: document.getElementById(\"mail_fulfillment\"),
|
|
});
|
|
}
|
|
} else {
|
|
document.getElementById(\"mail_fulfillment\").classList.add(\"hidden\");
|
|
document.getElementById(\"snippet_fulfillment\").classList.remove(\"hidden\");
|
|
|
|
if (send) {
|
|
fetch(\"/api/v1/products/{{ product.id }}/method\", {
|
|
method: \"POST\",
|
|
headers: {
|
|
\"Content-Type\": \"application/json\",
|
|
},
|
|
body: JSON.stringify({
|
|
method: \"ProfileStyle\",
|
|
}),
|
|
})
|
|
.then((res) => res.json())
|
|
.then((res) => {
|
|
trigger(\"atto::toast\", [
|
|
res.ok ? \"success\" : \"error\",
|
|
res.message,
|
|
]);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
setTimeout(() => {
|
|
mirror_use_automail();
|
|
mirror_fulfillment_style_select();
|
|
}, 150);"))
|
|
(text "{% endblock %}")
|