add: products ui
This commit is contained in:
parent
8f76578f1b
commit
fd529d3847
31 changed files with 1041 additions and 49 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -3318,7 +3318,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "14.0.0"
|
version = "15.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"async-stripe",
|
"async-stripe",
|
||||||
|
@ -3350,7 +3350,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "14.0.0"
|
version = "15.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base16ct",
|
"base16ct",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "14.0.0"
|
version = "15.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -69,6 +69,7 @@ pub const PROFILE_REPLIES: &str = include_str!("./public/html/profile/replies.li
|
||||||
pub const PROFILE_MEDIA: &str = include_str!("./public/html/profile/media.lisp");
|
pub const PROFILE_MEDIA: &str = include_str!("./public/html/profile/media.lisp");
|
||||||
pub const PROFILE_OUTBOX: &str = include_str!("./public/html/profile/outbox.lisp");
|
pub const PROFILE_OUTBOX: &str = include_str!("./public/html/profile/outbox.lisp");
|
||||||
pub const PROFILE_RESPONSES: &str = include_str!("./public/html/profile/responses.lisp");
|
pub const PROFILE_RESPONSES: &str = include_str!("./public/html/profile/responses.lisp");
|
||||||
|
pub const PROFILE_SHOP: &str = include_str!("./public/html/profile/shop.lisp");
|
||||||
|
|
||||||
pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.lisp");
|
pub const COMMUNITIES_LIST: &str = include_str!("./public/html/communities/list.lisp");
|
||||||
pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.lisp");
|
pub const COMMUNITIES_BASE: &str = include_str!("./public/html/communities/base.lisp");
|
||||||
|
@ -148,6 +149,9 @@ pub const MAIL_COMPOSE: &str = include_str!("./public/html/mail/compose.lisp");
|
||||||
pub const MAIL_LETTER: &str = include_str!("./public/html/mail/letter.lisp");
|
pub const MAIL_LETTER: &str = include_str!("./public/html/mail/letter.lisp");
|
||||||
|
|
||||||
pub const ECONOMY_WALLET: &str = include_str!("./public/html/economy/wallet.lisp");
|
pub const ECONOMY_WALLET: &str = include_str!("./public/html/economy/wallet.lisp");
|
||||||
|
pub const ECONOMY_PRODUCTS: &str = include_str!("./public/html/economy/products.lisp");
|
||||||
|
pub const ECONOMY_EDIT: &str = include_str!("./public/html/economy/edit.lisp");
|
||||||
|
pub const ECONOMY_PRODUCT: &str = include_str!("./public/html/economy/product.lisp");
|
||||||
|
|
||||||
// langs
|
// langs
|
||||||
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
|
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
|
||||||
|
@ -310,6 +314,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
||||||
write_template!(html_path->"profile/media.html"(crate::assets::PROFILE_MEDIA) --config=config --lisp plugins);
|
write_template!(html_path->"profile/media.html"(crate::assets::PROFILE_MEDIA) --config=config --lisp plugins);
|
||||||
write_template!(html_path->"profile/outbox.html"(crate::assets::PROFILE_OUTBOX) --config=config --lisp plugins);
|
write_template!(html_path->"profile/outbox.html"(crate::assets::PROFILE_OUTBOX) --config=config --lisp plugins);
|
||||||
write_template!(html_path->"profile/responses.html"(crate::assets::PROFILE_RESPONSES) --config=config --lisp plugins);
|
write_template!(html_path->"profile/responses.html"(crate::assets::PROFILE_RESPONSES) --config=config --lisp plugins);
|
||||||
|
write_template!(html_path->"profile/shop.html"(crate::assets::PROFILE_SHOP) --config=config --lisp plugins);
|
||||||
|
|
||||||
write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config --lisp plugins);
|
write_template!(html_path->"communities/list.html"(crate::assets::COMMUNITIES_LIST) -d "communities" --config=config --lisp plugins);
|
||||||
write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config --lisp plugins);
|
write_template!(html_path->"communities/base.html"(crate::assets::COMMUNITIES_BASE) --config=config --lisp plugins);
|
||||||
|
@ -382,6 +387,9 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
||||||
write_template!(html_path->"mail/letter.html"(crate::assets::MAIL_LETTER) --config=config --lisp plugins);
|
write_template!(html_path->"mail/letter.html"(crate::assets::MAIL_LETTER) --config=config --lisp plugins);
|
||||||
|
|
||||||
write_template!(html_path->"economy/wallet.html"(crate::assets::ECONOMY_WALLET) -d "economy" --config=config --lisp plugins);
|
write_template!(html_path->"economy/wallet.html"(crate::assets::ECONOMY_WALLET) -d "economy" --config=config --lisp plugins);
|
||||||
|
write_template!(html_path->"economy/products.html"(crate::assets::ECONOMY_PRODUCTS) --config=config --lisp plugins);
|
||||||
|
write_template!(html_path->"economy/edit.html"(crate::assets::ECONOMY_EDIT) --config=config --lisp plugins);
|
||||||
|
write_template!(html_path->"economy/product.html"(crate::assets::ECONOMY_PRODUCT) --config=config --lisp plugins);
|
||||||
|
|
||||||
html_path
|
html_path
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ version = "1.0.0"
|
||||||
"auth:label.replies" = "Replies"
|
"auth:label.replies" = "Replies"
|
||||||
"auth:label.media" = "Media"
|
"auth:label.media" = "Media"
|
||||||
"auth:label.outbox" = "Outbox"
|
"auth:label.outbox" = "Outbox"
|
||||||
|
"auth:label.shop" = "Shop"
|
||||||
"auth:label.before_you_view" = "Before you view"
|
"auth:label.before_you_view" = "Before you view"
|
||||||
"auth:label.private_profile" = "Private profile"
|
"auth:label.private_profile" = "Private profile"
|
||||||
"auth:label.private_profile_message" = "This profile is private, meaning you can only view it if they follow you."
|
"auth:label.private_profile_message" = "This profile is private, meaning you can only view it if they follow you."
|
||||||
|
@ -227,6 +228,7 @@ version = "1.0.0"
|
||||||
"requests:label.user_follow_request" = "User follow request"
|
"requests:label.user_follow_request" = "User follow request"
|
||||||
"requests:action.view_profile" = "View profile"
|
"requests:action.view_profile" = "View profile"
|
||||||
"requests:label.user_follow_request_message" = "Accepting this request will not allow them to see your profile. For that, you must follow them back."
|
"requests:label.user_follow_request_message" = "Accepting this request will not allow them to see your profile. For that, you must follow them back."
|
||||||
|
"requests:label.coin_transfer_request" = "Coin transfer request"
|
||||||
|
|
||||||
"chats:label.my_chats" = "My chats"
|
"chats:label.my_chats" = "My chats"
|
||||||
"chats:action.move" = "Move"
|
"chats:action.move" = "Move"
|
||||||
|
@ -327,3 +329,17 @@ version = "1.0.0"
|
||||||
"mail:action.send_mail" = "Send mail"
|
"mail:action.send_mail" = "Send mail"
|
||||||
|
|
||||||
"economy:label.recent_transfers" = "Recent transfers"
|
"economy:label.recent_transfers" = "Recent transfers"
|
||||||
|
"economy:action.request" = "Request"
|
||||||
|
"economy:label.title" = "Title"
|
||||||
|
"economy:label.description" = "Description"
|
||||||
|
"economy:label.my_products" = "My products"
|
||||||
|
"economy:label.my_wallet" = "My wallet"
|
||||||
|
"economy:label.create_new" = "Create new product"
|
||||||
|
"economy:label.price" = "Price"
|
||||||
|
"economy:label.on_sale" = "On sale"
|
||||||
|
"economy:label.stock" = "Stock"
|
||||||
|
"economy:label.unlimited" = "Unlimited"
|
||||||
|
"economy:label.fulfillment_style" = "Fulfillment style"
|
||||||
|
"economy:label.use_automail" = "Use automail"
|
||||||
|
"economy:label.automail_message" = "Automail message"
|
||||||
|
"economy:action.buy" = "Buy"
|
||||||
|
|
|
@ -483,6 +483,13 @@ select:focus {
|
||||||
border-color: var(--color-super-lowered);
|
border-color: var(--color-super-lowered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:disabled,
|
||||||
|
textarea:disabled,
|
||||||
|
select:disabled {
|
||||||
|
opacity: 50%;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.poll_bar {
|
.poll_bar {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
|
|
|
@ -2681,3 +2681,23 @@
|
||||||
(td (text "{{ ((post.likes + 1) / (post.dislikes + 1))|round(method=\"ceil\", precision=2) }}"))
|
(td (text "{{ ((post.likes + 1) / (post.dislikes + 1))|round(method=\"ceil\", precision=2) }}"))
|
||||||
(td (span ("class" "date short") (text "{{ post.created }}"))))
|
(td (span ("class" "date short") (text "{{ post.created }}"))))
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
||||||
|
(text "{% macro product_listing_card(product, owner=false, edit=false) -%}")
|
||||||
|
(a
|
||||||
|
("class" "card button lowered w_full flex flex_col gap_2")
|
||||||
|
("href" "/product/{{ product.id }}{% if edit -%} /edit {%- endif %}")
|
||||||
|
(text "{% if owner -%}")
|
||||||
|
(text "{{ self::full_username(user=owner) }}")
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(h3
|
||||||
|
("class" "flex gap_2 items_center {% if not product.on_sale -%} fade {%- endif %}")
|
||||||
|
("style" "height: 24px; text-decoration: {% if not product.on_sale -%} line-through {%- else -%} none {%- endif %}")
|
||||||
|
(icon (text "package"))
|
||||||
|
(text "{{ product.title }}"))
|
||||||
|
(h4
|
||||||
|
("class" "flex gap_2 items_center")
|
||||||
|
("style" "height: 18px")
|
||||||
|
(icon (text "badge-cent"))
|
||||||
|
(text "{{ product.price }}")))
|
||||||
|
(text "{%- endmacro %}")
|
||||||
|
|
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[3]) }}"))
|
||||||
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[4]) }}"))
|
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[4]) }}"))
|
||||||
(td
|
(td
|
||||||
|
("class" "flex items_center gap_1")
|
||||||
(text "{{ transfer[2] }}")
|
(text "{{ transfer[2] }}")
|
||||||
(text "{% if transfer[6] -%}")
|
(text "{% if transfer[6] -%}")
|
||||||
(span ("title" "Pending") (icon (text "clock")))
|
(span ("class" "flex items_center gap_1") ("title" "Pending") (icon (text "clock")))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
(td
|
(td
|
||||||
(text "{% if transfer[5] -%}")
|
(text "{% if transfer[5] -%}")
|
||||||
|
|
|
@ -73,14 +73,20 @@
|
||||||
("class" "inner")
|
("class" "inner")
|
||||||
(a
|
(a
|
||||||
("href" "/chats/0/0")
|
("href" "/chats/0/0")
|
||||||
("title" "Chats")
|
|
||||||
(icon (text "message-circle"))
|
(icon (text "message-circle"))
|
||||||
(str (text "communities:label.chats")))
|
(str (text "communities:label.chats")))
|
||||||
(a
|
(a
|
||||||
("href" "/mail")
|
("href" "/mail")
|
||||||
("title" "Mail")
|
|
||||||
(icon (text "mail"))
|
(icon (text "mail"))
|
||||||
(str (text "general:link.mail")))
|
(str (text "general:link.mail")))
|
||||||
|
(a
|
||||||
|
("href" "/wallet")
|
||||||
|
(icon (text "piggy-bank"))
|
||||||
|
(str (text "economy:label.my_wallet")))
|
||||||
|
(a
|
||||||
|
("href" "/products")
|
||||||
|
(icon (text "store"))
|
||||||
|
(str (text "economy:label.my_products")))
|
||||||
(a
|
(a
|
||||||
("href" "/journals/0/0")
|
("href" "/journals/0/0")
|
||||||
(icon (text "notebook"))
|
(icon (text "notebook"))
|
||||||
|
@ -318,6 +324,13 @@
|
||||||
("class" "{% if selected == 'media' -%}active{%- endif %}")
|
("class" "{% if selected == 'media' -%}active{%- endif %}")
|
||||||
(str (text "auth:label.media")))
|
(str (text "auth:label.media")))
|
||||||
|
|
||||||
|
(text "{% if user and profile.settings.enable_shop -%}")
|
||||||
|
(a
|
||||||
|
("href" "/@{{ profile.username }}/shop")
|
||||||
|
("class" "{% if selected == 'shop' -%}active{%- endif %}")
|
||||||
|
(str (text "auth:label.shop")))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
(text "{% if is_self or is_helper -%}")
|
(text "{% if is_self or is_helper -%}")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ profile.username }}/outbox")
|
("href" "/@{{ profile.username }}/outbox")
|
||||||
|
|
|
@ -112,6 +112,7 @@
|
||||||
subject: e.target.subject.value.trim(),
|
subject: e.target.subject.value.trim(),
|
||||||
receivers: RECEIVERS,
|
receivers: RECEIVERS,
|
||||||
replying_to: SEARCH_PARAMS.get(\"replying_to\") || \"0\",
|
replying_to: SEARCH_PARAMS.get(\"replying_to\") || \"0\",
|
||||||
|
transfer_id: SEARCH_PARAMS.get(\"transfer_id\") || \"0\",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|
|
@ -92,6 +92,37 @@
|
||||||
(text "{{ icon \"trash\" }}")
|
(text "{{ icon \"trash\" }}")
|
||||||
(span
|
(span
|
||||||
(text "{{ text \"general:action.delete\" }}"))))))
|
(text "{{ text \"general:action.delete\" }}"))))))
|
||||||
|
(text "{% elif request.action_type == \"Transfer\" %}")
|
||||||
|
(div
|
||||||
|
("class" "card_nest")
|
||||||
|
(div
|
||||||
|
("class" "card small flex items_center gap_2")
|
||||||
|
(icon (text "piggy-bank"))
|
||||||
|
(span
|
||||||
|
(str (text "requests:label.coin_transfer_request"))))
|
||||||
|
(div
|
||||||
|
("class" "card flex flex_col gap_2")
|
||||||
|
(span (a ("href" "/api/v1/auth/user/find/{{ request.linked_asset }}") (text "Somebody")) (text " is asking for a transfer of ") (b (text "{{ request.data.Int32 }} coins")) (text "."))
|
||||||
|
(div
|
||||||
|
("class" "card flex flex_wrap w_full secondary gap_2")
|
||||||
|
(a
|
||||||
|
("href" "/api/v1/auth/user/find/{{ request.linked_asset }}")
|
||||||
|
("class" "button")
|
||||||
|
(text "{{ icon \"external-link\" }}")
|
||||||
|
(span
|
||||||
|
(text "{{ text \"requests:action.view_profile\" }}")))
|
||||||
|
(button
|
||||||
|
("class" "lowered green")
|
||||||
|
("onclick" "accept_transfer_request(event, '{{ request.id }}', '{{ request.linked_asset }}', {{ request.data.Int32 }})")
|
||||||
|
(text "{{ icon \"check\" }}")
|
||||||
|
(span
|
||||||
|
(text "{{ text \"general:action.accept\" }}")))
|
||||||
|
(button
|
||||||
|
("class" "lowered red")
|
||||||
|
("onclick" "remove_request('{{ request.id }}', '{{ request.linked_asset }}')")
|
||||||
|
(text "{{ icon \"trash\" }}")
|
||||||
|
(span
|
||||||
|
(text "{{ text \"general:action.delete\" }}"))))))
|
||||||
(text "{%- endif %} {% endfor %} {% for question in questions %}")
|
(text "{%- endif %} {% endfor %} {% for question in questions %}")
|
||||||
(div
|
(div
|
||||||
("class" "card_nest")
|
("class" "card_nest")
|
||||||
|
@ -138,13 +169,15 @@
|
||||||
(text "{{ components::pagination(page=page, items=requests|length, key=\"&id=\", value=profile.id) }}"))
|
(text "{{ components::pagination(page=page, items=requests|length, key=\"&id=\", value=profile.id) }}"))
|
||||||
|
|
||||||
(script
|
(script
|
||||||
(text "async function remove_request(id, linked_asset) {
|
(text "async function remove_request(id, linked_asset, confirm = true) {
|
||||||
if (
|
if (confirm) {
|
||||||
!(await trigger(\"atto::confirm\", [
|
if (
|
||||||
\"Are you sure you want to do this?\",
|
!(await trigger(\"atto::confirm\", [
|
||||||
]))
|
\"Are you sure you want to do this?\",
|
||||||
) {
|
]))
|
||||||
return;
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`/api/v1/requests/${id}/${linked_asset}`, {
|
fetch(`/api/v1/requests/${id}/${linked_asset}`, {
|
||||||
|
@ -275,6 +308,41 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.accept_transfer_request = async (e, id, receiver, amount) => {
|
||||||
|
await trigger(\"atto::debounce\", [\"economy::transfer\"]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await trigger(\"atto::confirm\", [
|
||||||
|
\"Are you sure you would like to do this?\",
|
||||||
|
]))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/transfers`, {
|
||||||
|
method: \"POST\",
|
||||||
|
headers: {
|
||||||
|
\"Content-Type\": \"application/json\",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
receiver,
|
||||||
|
amount,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(async (res) => {
|
||||||
|
trigger(\"atto::toast\", [
|
||||||
|
res.ok ? \"success\" : \"error\",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
e.target.parentElement.parentElement.parentElement.parentElement.remove();
|
||||||
|
remove_request(id, receiver, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
};"))
|
};"))
|
||||||
|
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -256,6 +256,13 @@
|
||||||
(icon (text "mail-plus"))
|
(icon (text "mail-plus"))
|
||||||
(span
|
(span
|
||||||
(str (text "mail:action.send_mail"))))
|
(str (text "mail:action.send_mail"))))
|
||||||
|
(text "{%- endif %} {% if not profile.settings.no_transfers -%}")
|
||||||
|
(button
|
||||||
|
("onclick" "request_transfer()")
|
||||||
|
("class" "lowered")
|
||||||
|
(icon (text "badge-cent"))
|
||||||
|
(span
|
||||||
|
(str (text "economy:action.request"))))
|
||||||
(text "{%- endif %} {% if is_helper -%}")
|
(text "{%- endif %} {% if is_helper -%}")
|
||||||
(a
|
(a
|
||||||
("href" "/mod_panel/profile/{{ profile.id }}")
|
("href" "/mod_panel/profile/{{ profile.id }}")
|
||||||
|
@ -289,6 +296,41 @@
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
globalThis.request_transfer = async () => {
|
||||||
|
await trigger(\"atto::debounce\", [\"economy::transfer\"]);
|
||||||
|
const amount = Number.parseInt((await trigger(\"atto::prompt\", [\"Request amount:\"])) || \"\");
|
||||||
|
|
||||||
|
if (amount === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await trigger(\"atto::confirm\", [
|
||||||
|
`Are you sure you would like to request ${amount} coins from {{ profile.username }}?`,
|
||||||
|
]))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/transfers/ask`, {
|
||||||
|
method: \"POST\",
|
||||||
|
headers: {
|
||||||
|
\"Content-Type\": \"application/json\",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
receiver: \"{{ profile.id }}\",
|
||||||
|
amount,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(async (res) => {
|
||||||
|
trigger(\"atto::toast\", [
|
||||||
|
res.ok ? \"success\" : \"error\",
|
||||||
|
res.message,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
globalThis.toggle_follow_user = async (e) => {
|
globalThis.toggle_follow_user = async (e) => {
|
||||||
await trigger(\"atto::debounce\", [
|
await trigger(\"atto::debounce\", [
|
||||||
\"users::follow\",
|
\"users::follow\",
|
||||||
|
|
|
@ -1958,6 +1958,23 @@
|
||||||
settings.forum_signature,
|
settings.forum_signature,
|
||||||
\"textarea\",
|
\"textarea\",
|
||||||
],
|
],
|
||||||
|
[[], \"Economy\", \"title\"],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
\"enable_shop\",
|
||||||
|
\"Show shop tab on my profile\",
|
||||||
|
],
|
||||||
|
\"{{ profile.settings.enable_shop }}\",
|
||||||
|
\"checkbox\",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
\"no_transfers\",
|
||||||
|
\"Disable transfer requests\",
|
||||||
|
],
|
||||||
|
\"{{ profile.settings.no_transfers }}\",
|
||||||
|
\"checkbox\",
|
||||||
|
],
|
||||||
[[], \"Misc\", \"title\"],
|
[[], \"Misc\", \"title\"],
|
||||||
[
|
[
|
||||||
[\"hide_dislikes\", \"Hide post dislikes\"],
|
[\"hide_dislikes\", \"Hide post dislikes\"],
|
||||||
|
|
17
crates/app/src/public/html/profile/shop.lisp
Normal file
17
crates/app/src/public/html/profile/shop.lisp
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
(text "{% extends \"profile/base.html\" %} {% block content %} {% if profile.settings.enable_questions and (user or profile.settings.allow_anonymous_questions) %}")
|
||||||
|
(div
|
||||||
|
("style" "display: contents")
|
||||||
|
(text "{{ components::create_question_form(receiver=profile.id, header=profile.settings.motivational_header, drawing_enabled=profile.settings.enable_drawings, allow_anonymous=profile.settings.allow_anonymous_questions) }}"))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
(text "{{ macros::profile_nav(selected=\"shop\") }}")
|
||||||
|
(div
|
||||||
|
("class" "card_nest")
|
||||||
|
(div
|
||||||
|
("class" "card small flex gap_2 items_center")
|
||||||
|
(icon (text "store"))
|
||||||
|
(str (text "auth:label.shop")))
|
||||||
|
(div
|
||||||
|
("class" "card w_full flex flex_col gap_2")
|
||||||
|
(text "{% for item in list %} {{ components::product_listing_card(product=item) }} {% endfor %}")
|
||||||
|
(text "{{ components::pagination(page=page, items=list|length) }}")))
|
||||||
|
(text "{% endblock %}")
|
|
@ -85,6 +85,10 @@ media_theme_pref();
|
||||||
element.removeAttribute("checked");
|
element.removeAttribute("checked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const element of document.querySelectorAll('[disabled="false"]')) {
|
||||||
|
element.removeAttribute("disabled");
|
||||||
|
}
|
||||||
|
|
||||||
for (const element of document.querySelectorAll('[selected="true"]')) {
|
for (const element of document.querySelectorAll('[selected="true"]')) {
|
||||||
element.parentElement.value = element.value;
|
element.parentElement.value = element.value;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use axum::{
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
Extension, Json,
|
Extension, Json,
|
||||||
};
|
};
|
||||||
use tetratto_core::model::{auth::Notification, mail::Letter, oauth, ApiReturn, Error};
|
use tetratto_core::model::{mail::Letter, oauth, ApiReturn, Error};
|
||||||
use crate::{cookie::CookieJar, get_user_from_token, routes::pages::PaginatedQuery, State};
|
use crate::{cookie::CookieJar, get_user_from_token, routes::pages::PaginatedQuery, State};
|
||||||
use super::CreateLetter;
|
use super::CreateLetter;
|
||||||
|
|
||||||
|
@ -170,30 +170,19 @@ pub async fn create_request(
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(l) => {
|
Ok(l) => {
|
||||||
// send notifications
|
// check if we're fulfilling a coin transfer
|
||||||
for x in &l.receivers {
|
if !props.transfer_id.is_empty() && props.transfer_id != "0" {
|
||||||
if let Err(e) = data
|
if let Err(e) = data
|
||||||
.create_notification(Notification::new(
|
.apply_transfer(match props.transfer_id.parse() {
|
||||||
"You've got mail!".to_string(),
|
Ok(x) => x,
|
||||||
format!(
|
Err(_) => return Json(Error::Unknown.into()),
|
||||||
"[@{}](/api/v1/auth/user/find/{}) has sent you a [letter](/mail/letter/{}).",
|
})
|
||||||
user.username, user.id, l.id
|
|
||||||
),
|
|
||||||
*x,
|
|
||||||
))
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Json(e.into());
|
return Json(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we're fulfilling a coin transfer
|
|
||||||
if props.transfer_id != 0 {
|
|
||||||
if let Err(e) = data.apply_transfer(props.transfer_id).await {
|
|
||||||
return Json(e.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
Json(ApiReturn {
|
Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
|
@ -710,6 +710,7 @@ pub fn routes() -> Router {
|
||||||
.route("/letters/received", get(letters::list_received_request))
|
.route("/letters/received", get(letters::list_received_request))
|
||||||
// transfers
|
// transfers
|
||||||
.route("/transfers", post(transfers::create_request))
|
.route("/transfers", post(transfers::create_request))
|
||||||
|
.route("/transfers/ask", post(transfers::ask_request))
|
||||||
// products
|
// products
|
||||||
.route("/products", post(products::create_request))
|
.route("/products", post(products::create_request))
|
||||||
.route("/products/{id}", delete(products::delete_request))
|
.route("/products/{id}", delete(products::delete_request))
|
||||||
|
@ -1236,12 +1237,12 @@ pub struct CreateLetter {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub replying_to: String,
|
pub replying_to: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub transfer_id: usize,
|
pub transfer_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateCoinTransfer {
|
pub struct CreateCoinTransfer {
|
||||||
pub receiver: usize,
|
pub receiver: String,
|
||||||
pub amount: i32,
|
pub amount: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,15 @@ pub async fn update_price_request(
|
||||||
None => return Json(Error::NotAllowed.into()),
|
None => return Json(Error::NotAllowed.into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if req.price < 25 {
|
||||||
|
return Json(
|
||||||
|
Error::MiscError(
|
||||||
|
"Price is too low, please a price of use 25 coins or more".to_string(),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
match data.update_product_price(id, &user, req.price).await {
|
match data.update_product_price(id, &user, req.price).await {
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|
|
@ -2,7 +2,9 @@ use crate::{get_user_from_token, State, cookie::CookieJar};
|
||||||
use axum::{response::IntoResponse, Extension, Json};
|
use axum::{response::IntoResponse, Extension, Json};
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
economy::{CoinTransfer, CoinTransferMethod},
|
economy::{CoinTransfer, CoinTransferMethod},
|
||||||
oauth, ApiReturn, Error,
|
oauth,
|
||||||
|
requests::{ActionData, ActionRequest, ActionType},
|
||||||
|
ApiReturn, Error,
|
||||||
};
|
};
|
||||||
use super::CreateCoinTransfer;
|
use super::CreateCoinTransfer;
|
||||||
|
|
||||||
|
@ -21,7 +23,10 @@ pub async fn create_request(
|
||||||
.create_transfer(
|
.create_transfer(
|
||||||
&mut CoinTransfer::new(
|
&mut CoinTransfer::new(
|
||||||
user.id,
|
user.id,
|
||||||
req.receiver,
|
match req.receiver.parse() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => return Json(Error::Unknown.into()),
|
||||||
|
},
|
||||||
req.amount,
|
req.amount,
|
||||||
CoinTransferMethod::Transfer, // this endpoint is ONLY for regular transfers; products export a buy endpoint for the other method
|
CoinTransferMethod::Transfer, // this endpoint is ONLY for regular transfers; products export a buy endpoint for the other method
|
||||||
),
|
),
|
||||||
|
@ -37,3 +42,35 @@ pub async fn create_request(
|
||||||
Err(e) => Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn ask_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Json(req): Json<CreateCoinTransfer>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserSendCoins) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
match data
|
||||||
|
.create_request(ActionRequest::new(
|
||||||
|
match req.receiver.parse() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => return Json(Error::Unknown.into()),
|
||||||
|
},
|
||||||
|
ActionType::Transfer,
|
||||||
|
user.id,
|
||||||
|
Some(ActionData::Int32(req.amount)),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Asked user for transfer".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -43,3 +43,109 @@ pub async fn wallet_request(
|
||||||
data.1.render("economy/wallet.html", &context).unwrap(),
|
data.1.render("economy/wallet.html", &context).unwrap(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `/products`
|
||||||
|
pub async fn products_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Query(props): Query<PaginatedQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = data.read().await;
|
||||||
|
let user = match get_user_from_token!(jar, data.0) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let list = match data.0.get_products_by_user(user.id, 12, props.page).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let lang = get_lang!(jar, data.0);
|
||||||
|
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||||
|
|
||||||
|
context.insert("list", &list);
|
||||||
|
context.insert("page", &props.page);
|
||||||
|
|
||||||
|
// return
|
||||||
|
Ok(Html(
|
||||||
|
data.1.render("economy/products.html", &context).unwrap(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `/product/{id}/edit`
|
||||||
|
pub async fn edit_product_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = data.read().await;
|
||||||
|
let user = match get_user_from_token!(jar, data.0) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let product = match data.0.get_product_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.id != product.owner {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let lang = get_lang!(jar, data.0);
|
||||||
|
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||||
|
context.insert("product", &product);
|
||||||
|
|
||||||
|
// return
|
||||||
|
Ok(Html(data.1.render("economy/edit.html", &context).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `/product/{id}`
|
||||||
|
pub async fn product_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = data.read().await;
|
||||||
|
let user = match get_user_from_token!(jar, data.0) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let product = match data.0.get_product_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let owner = match data.0.get_user_by_id(product.owner).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let lang = get_lang!(jar, data.0);
|
||||||
|
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
|
||||||
|
|
||||||
|
context.insert("product", &product);
|
||||||
|
context.insert("owner", &owner);
|
||||||
|
|
||||||
|
// return
|
||||||
|
Ok(Html(
|
||||||
|
data.1.render("economy/product.html", &context).unwrap(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ pub fn routes() -> Router {
|
||||||
.route("/@{username}/replies", get(profile::replies_request))
|
.route("/@{username}/replies", get(profile::replies_request))
|
||||||
.route("/@{username}/following", get(profile::following_request))
|
.route("/@{username}/following", get(profile::following_request))
|
||||||
.route("/@{username}/followers", get(profile::followers_request))
|
.route("/@{username}/followers", get(profile::followers_request))
|
||||||
|
.route("/@{username}/shop", get(profile::shop_request))
|
||||||
// communities
|
// communities
|
||||||
.route("/communities", get(communities::list_request))
|
.route("/communities", get(communities::list_request))
|
||||||
.route("/communities/search", get(communities::search_request))
|
.route("/communities/search", get(communities::search_request))
|
||||||
|
@ -162,6 +163,9 @@ pub fn routes() -> Router {
|
||||||
.route("/mail/letter/{id}", get(mail::letter_request))
|
.route("/mail/letter/{id}", get(mail::letter_request))
|
||||||
// economy
|
// economy
|
||||||
.route("/wallet", get(economy::wallet_request))
|
.route("/wallet", get(economy::wallet_request))
|
||||||
|
.route("/products", get(economy::products_request))
|
||||||
|
.route("/product/{id}/edit", get(economy::edit_product_request))
|
||||||
|
.route("/product/{id}", get(economy::product_request))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lw_routes() -> Router {
|
pub fn lw_routes() -> Router {
|
||||||
|
|
|
@ -307,20 +307,20 @@ pub async fn posts_request(
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(p) => Some(data.0.posts_muted_phrase_filter(
|
Ok(p) => data.0.posts_muted_phrase_filter(
|
||||||
&p,
|
&p,
|
||||||
if let Some(ref ua) = user {
|
if let Some(ref ua) = user {
|
||||||
Some(&ua.settings.muted)
|
Some(&ua.settings.muted)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
},
|
},
|
||||||
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
|
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
|
||||||
|
@ -614,6 +614,95 @@ pub async fn media_request(
|
||||||
Ok(Html(data.1.render("profile/media.html", &context).unwrap()))
|
Ok(Html(data.1.render("profile/media.html", &context).unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `/@{username}/shop`
|
||||||
|
pub async fn shop_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Path(username): Path<String>,
|
||||||
|
Query(props): Query<PaginatedQuery>,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = data.read().await;
|
||||||
|
let user = match get_user_from_token!(jar, data.0) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &None).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let other_user = match data.0.get_user_by_username(&username).await {
|
||||||
|
Ok(ua) => ua,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !other_user.settings.enable_shop {
|
||||||
|
return Err(Html(
|
||||||
|
render_error(Error::NotAllowed, &jar, &data, &Some(user)).await,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
check_user_blocked_or_private!(Some(user.clone()), other_user, data, jar);
|
||||||
|
|
||||||
|
// fetch data
|
||||||
|
let list = match data
|
||||||
|
.0
|
||||||
|
.get_products_by_user(other_user.id, 12, props.page)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
|
||||||
|
Ok(m) => match data.0.fill_communities(m).await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
},
|
||||||
|
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// init context
|
||||||
|
let lang = get_lang!(jar, data.0);
|
||||||
|
let mut context = initial_context(&data.0.0.0, lang, &Some(user.clone())).await;
|
||||||
|
|
||||||
|
let is_self = user.id == other_user.id;
|
||||||
|
|
||||||
|
let is_following = data
|
||||||
|
.0
|
||||||
|
.get_userfollow_by_initiator_receiver(user.id, other_user.id)
|
||||||
|
.await
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
|
let is_following_you = data
|
||||||
|
.0
|
||||||
|
.get_userfollow_by_receiver_initiator(user.id, other_user.id)
|
||||||
|
.await
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
|
let is_blocking = data
|
||||||
|
.0
|
||||||
|
.get_userblock_by_initiator_receiver(user.id, other_user.id)
|
||||||
|
.await
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
|
context.insert("list", &list);
|
||||||
|
context.insert("page", &props.page);
|
||||||
|
profile_context(
|
||||||
|
&mut context,
|
||||||
|
&Some(user),
|
||||||
|
&other_user,
|
||||||
|
&communities,
|
||||||
|
is_self,
|
||||||
|
is_following,
|
||||||
|
is_following_you,
|
||||||
|
is_blocking,
|
||||||
|
);
|
||||||
|
|
||||||
|
// return
|
||||||
|
Ok(Html(data.1.render("profile/shop.html", &context).unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
/// `/@{username}/outbox`
|
/// `/@{username}/outbox`
|
||||||
pub async fn outbox_request(
|
pub async fn outbox_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
description = "The core behind Tetratto"
|
description = "The core behind Tetratto"
|
||||||
version = "14.0.0"
|
version = "15.0.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -401,6 +401,7 @@ fn default_banned_usernames() -> Vec<String> {
|
||||||
"mail".to_string(),
|
"mail".to_string(),
|
||||||
"product".to_string(),
|
"product".to_string(),
|
||||||
"wallet".to_string(),
|
"wallet".to_string(),
|
||||||
|
"products".to_string(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::model::auth::Notification;
|
||||||
use crate::model::{auth::User, mail::Letter, permissions::SecondaryPermission, Error, Result};
|
use crate::model::{auth::User, mail::Letter, permissions::SecondaryPermission, Error, Result};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
|
use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
|
||||||
|
@ -160,6 +161,9 @@ impl DataManager {
|
||||||
return Err(Error::DataTooLong("receivers".to_string()));
|
return Err(Error::DataTooLong("receivers".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get sender
|
||||||
|
let sender = self.get_user_by_id(data.owner).await?;
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
let conn = match self.0.connect().await {
|
let conn = match self.0.connect().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
|
@ -185,6 +189,20 @@ impl DataManager {
|
||||||
return Err(Error::DatabaseError(e.to_string()));
|
return Err(Error::DatabaseError(e.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send notifications
|
||||||
|
for x in &data.receivers {
|
||||||
|
self.create_notification(Notification::new(
|
||||||
|
"You've got mail!".to_string(),
|
||||||
|
format!(
|
||||||
|
"[@{}](/api/v1/auth/user/find/{}) has sent you a [letter](/mail/letter/{}).",
|
||||||
|
sender.username, sender.id, data.id
|
||||||
|
),
|
||||||
|
*x,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@ impl DataManager {
|
||||||
match product.method {
|
match product.method {
|
||||||
ProductFulfillmentMethod::AutoMail(message) => {
|
ProductFulfillmentMethod::AutoMail(message) => {
|
||||||
// we're basically done, transfer coins and send mail
|
// we're basically done, transfer coins and send mail
|
||||||
self.create_transfer(&mut transfer, false).await?;
|
self.create_transfer(&mut transfer, true).await?;
|
||||||
|
|
||||||
self.create_letter(Letter::new(
|
self.create_letter(Letter::new(
|
||||||
self.0.0.system_user,
|
self.0.0.system_user,
|
||||||
|
@ -167,6 +167,16 @@ impl DataManager {
|
||||||
// mark transfer as pending and create it
|
// mark transfer as pending and create it
|
||||||
self.create_transfer(&mut transfer, false).await?;
|
self.create_transfer(&mut transfer, false).await?;
|
||||||
|
|
||||||
|
// tell the customer to wait
|
||||||
|
self.create_letter(Letter::new(
|
||||||
|
self.0.0.system_user,
|
||||||
|
vec![customer.id],
|
||||||
|
format!("Thank you for purchasing \"{}\"", product.title),
|
||||||
|
"This product uses manual mail, meaning you won't be charged until the product owner sends you a letter about the product. You'll see a pending transfer in your wallet.".to_string(),
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
// tell product owner they have a new pending purchase
|
// tell product owner they have a new pending purchase
|
||||||
self.create_letter(Letter::new(
|
self.create_letter(Letter::new(
|
||||||
self.0.0.system_user,
|
self.0.0.system_user,
|
||||||
|
@ -178,7 +188,7 @@ impl DataManager {
|
||||||
If your product is a purchase of goods or services, please be sure to fulfill this purchase either in the letter or elsewhere. The customer may request support if you fail to do so.
|
If your product is a purchase of goods or services, please be sure to fulfill this purchase either in the letter or elsewhere. The customer may request support if you fail to do so.
|
||||||
|
|
||||||
***
|
***
|
||||||
<a class=\"button\" href=\"/mail/compose?receivers=id:{}&title=Product%20fulfillment&transfer_id={}\">Fulfill purchase</a>",
|
<a class=\"button\" href=\"/mail/compose?receivers=id:{}&subject=Product%20fulfillment&transfer_id={}\">Fulfill purchase</a>",
|
||||||
product.id, product.title, customer.id, transfer.id
|
product.id, product.title, customer.id, transfer.id
|
||||||
),
|
),
|
||||||
0,
|
0,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use crate::model::{
|
||||||
use crate::model::auth::User;
|
Error, Result,
|
||||||
use crate::model::economy::{CoinTransferMethod, Product};
|
economy::{CoinTransferMethod, Product, CoinTransfer},
|
||||||
use crate::model::{Error, Result, economy::CoinTransfer};
|
auth::{Notification, User},
|
||||||
|
};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
|
use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
|
||||||
|
|
||||||
|
@ -168,8 +169,35 @@ impl DataManager {
|
||||||
self.update_user_coins(sender.id, sender.coins).await?;
|
self.update_user_coins(sender.id, sender.coins).await?;
|
||||||
self.update_user_coins(receiver.id, receiver.coins).await?;
|
self.update_user_coins(receiver.id, receiver.coins).await?;
|
||||||
self.update_transfer_is_pending(id, 0).await?;
|
self.update_transfer_is_pending(id, 0).await?;
|
||||||
|
|
||||||
|
self.create_notification(Notification::new(
|
||||||
|
"Purchase fulfilled!".to_string(),
|
||||||
|
format!(
|
||||||
|
"You've just successfully fulfilled a purchase for a [product](/product/{}).",
|
||||||
|
match transfer.method {
|
||||||
|
CoinTransferMethod::Purchase(x) => x,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
receiver.id,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.create_notification(Notification::new(
|
||||||
|
"Purchase fulfilled!".to_string(),
|
||||||
|
format!(
|
||||||
|
"Your purchase for a [product](/product/{}) has been fulfilled.",
|
||||||
|
match transfer.method {
|
||||||
|
CoinTransferMethod::Purchase(x) => x,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
sender.id,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_method!(update_transfer_is_pending(i32) -> "UPDATE products SET is_pending = $1 WHERE id = $2" --cache-key-tmpl="atto.transfer:{}");
|
auto_method!(update_transfer_is_pending(i32) -> "UPDATE transfers SET is_pending = $1 WHERE id = $2" --cache-key-tmpl="atto.transfer:{}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -350,6 +350,12 @@ pub struct UserSettings {
|
||||||
/// The signature automatically attached to new forum posts.
|
/// The signature automatically attached to new forum posts.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub forum_signature: String,
|
pub forum_signature: String,
|
||||||
|
/// If coin transfer requests are disabled.
|
||||||
|
#[serde(default)]
|
||||||
|
pub no_transfers: bool,
|
||||||
|
/// If your profile has the "Shop" tab enabled.
|
||||||
|
#[serde(default)]
|
||||||
|
pub enable_shop: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mime_avif() -> String {
|
fn mime_avif() -> String {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum ActionData {
|
pub enum ActionData {
|
||||||
String(String),
|
String(String),
|
||||||
Int32(i32),
|
Int32(i32),
|
||||||
|
@ -46,7 +46,7 @@ impl Default for ActionData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum ActionType {
|
pub enum ActionType {
|
||||||
/// A request to join a community.
|
/// A request to join a community.
|
||||||
///
|
///
|
||||||
|
@ -66,7 +66,7 @@ pub enum ActionType {
|
||||||
Transfer,
|
Transfer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct ActionRequest {
|
pub struct ActionRequest {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub created: usize,
|
pub created: usize,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue