add: products ui
This commit is contained in:
parent
8f76578f1b
commit
fd529d3847
31 changed files with 1041 additions and 49 deletions
322
crates/app/src/public/html/economy/edit.lisp
Normal file
322
crates/app/src/public/html/economy/edit.lisp
Normal file
|
@ -0,0 +1,322 @@
|
|||
(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 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"))))
|
||||
(div
|
||||
("class" "flex flex_col gap_1")
|
||||
(label
|
||||
("for" "title")
|
||||
(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" "title")
|
||||
(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"))))
|
||||
(form
|
||||
("class" "card flex flex_col gap_2")
|
||||
("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."))
|
||||
|
||||
(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 product.method != \"ManualMail\" -%} 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 product.method != \"ManualMail\" -%} {{ product.method.AutoMail }} {%- endif %}")))
|
||||
(button (str (text "general:action.save")))))
|
||||
|
||||
(a
|
||||
("class" "button secondary")
|
||||
("href" "/product/{{ product.id }}")
|
||||
(icon (text "arrow-left"))
|
||||
(str (text "general:action.back"))))
|
||||
|
||||
(script
|
||||
(text "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_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,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
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\");
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
mirror_use_automail();
|
||||
}, 150);"))
|
||||
(text "{% endblock %}")
|
67
crates/app/src/public/html/economy/product.lisp
Normal file
67
crates/app/src/public/html/economy/product.lisp
Normal file
|
@ -0,0 +1,67 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "{{ product.title }} - {{ config.name }}"))
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"\") }}")
|
||||
(main
|
||||
("class" "flex flex_col gap_2")
|
||||
(div
|
||||
("class" "card flex flex_col gap_2")
|
||||
(h3
|
||||
("style" "height: 32px")
|
||||
(text "{{ product.title }}"))
|
||||
(text "{{ components::full_username(user=owner) }}")
|
||||
|
||||
(text "{% if product.stock >= 0 -%}")
|
||||
(span ("class" "red") (text "{{ product.stock }} remaining"))
|
||||
(text "{%- endif %}")
|
||||
|
||||
(div
|
||||
("class" "card lowered w_full no_p_margin")
|
||||
(text "{{ product.description|markdown|safe }}"))
|
||||
|
||||
(div
|
||||
("class" "flex gap_2 items_center")
|
||||
(a
|
||||
("class" "button camo lowered")
|
||||
("href" "/wallet")
|
||||
("target" "_blank")
|
||||
(icon (text "badge-cent"))
|
||||
(text "{{ product.price }}"))
|
||||
(text "{% if user.id != product.owner -%}")
|
||||
(button
|
||||
("onclick" "purchase()")
|
||||
("disabled" "{{ product.stock == 0 }}")
|
||||
(icon (text "piggy-bank"))
|
||||
(str (text "economy:action.buy")))
|
||||
(text "{% else %}")
|
||||
(a
|
||||
("class" "button")
|
||||
("href" "/product/{{ product.id }}/edit")
|
||||
(icon (text "settings"))
|
||||
(str (text "general:label.edit")))
|
||||
(text "{%- endif %}"))))
|
||||
|
||||
(script
|
||||
(text "async function purchase() {
|
||||
await trigger(\"atto::debounce\", [\"products::buy\"]);
|
||||
|
||||
if (
|
||||
!(await trigger(\"atto::confirm\", [
|
||||
\"Are you sure you would like to do this? Your new balance will be {{ user.coins - product.price }} coins.\",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(\"/api/v1/products/{{ product.id }}/buy\", {
|
||||
method: \"POST\",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
});
|
||||
}"))
|
||||
(text "{% endblock %}")
|
91
crates/app/src/public/html/economy/products.lisp
Normal file
91
crates/app/src/public/html/economy/products.lisp
Normal file
|
@ -0,0 +1,91 @@
|
|||
(text "{% extends \"root.html\" %} {% block head %}")
|
||||
(title
|
||||
(text "My products - {{ config.name }}"))
|
||||
|
||||
(text "{% endblock %} {% block body %} {{ macros::nav(selected=\"products\") }}")
|
||||
(main
|
||||
("class" "flex flex_col gap_2")
|
||||
; create new
|
||||
(text "{{ components::supporter_ad(body=\"Become a supporter to create unlimited products!\") }}")
|
||||
|
||||
(div
|
||||
("class" "card_nest")
|
||||
(div
|
||||
("class" "card small")
|
||||
(b
|
||||
(str (text "economy:label.create_new"))))
|
||||
(form
|
||||
("class" "card flex flex_col gap_2")
|
||||
("onsubmit" "create_product_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")))
|
||||
(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")))
|
||||
(button
|
||||
(text "{{ text \"communities:action.create\" }}"))))
|
||||
|
||||
; product listing
|
||||
(div
|
||||
("class" "card_nest")
|
||||
(div
|
||||
("class" "card small flex items_center gap_2")
|
||||
(icon (text "store"))
|
||||
(str (text "economy:label.my_products")))
|
||||
|
||||
(div
|
||||
("class" "card flex flex_col gap_2")
|
||||
(text "{% for item in list %} {{ components::product_listing_card(product=item, edit=true) }} {% endfor %}")
|
||||
(text "{{ components::pagination(page=page, items=list|length) }}"))))
|
||||
|
||||
(script
|
||||
(text "async function create_product_from_form(e) {
|
||||
e.preventDefault();
|
||||
await trigger(\"atto::debounce\", [\"products::create\"]);
|
||||
|
||||
fetch(\"/api/v1/products\", {
|
||||
method: \"POST\",
|
||||
headers: {
|
||||
\"Content-Type\": \"application/json\",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: e.target.title.value,
|
||||
description: e.target.description.value,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger(\"atto::toast\", [
|
||||
res.ok ? \"success\" : \"error\",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
e.target.reset();
|
||||
setTimeout(() => {
|
||||
window.location.href = `/product/${res.payload}/edit`;
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}"))
|
||||
(text "{% endblock %}")
|
|
@ -51,9 +51,10 @@
|
|||
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[3]) }}"))
|
||||
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[4]) }}"))
|
||||
(td
|
||||
("class" "flex items_center gap_1")
|
||||
(text "{{ transfer[2] }}")
|
||||
(text "{% if transfer[6] -%}")
|
||||
(span ("title" "Pending") (icon (text "clock")))
|
||||
(span ("class" "flex items_center gap_1") ("title" "Pending") (icon (text "clock")))
|
||||
(text "{%- endif %}"))
|
||||
(td
|
||||
(text "{% if transfer[5] -%}")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue