add: ProfileStyle products

This commit is contained in:
trisua 2025-08-08 23:44:45 -04:00
parent 077e9252e3
commit 95cb889080
19 changed files with 525 additions and 54 deletions

View file

@ -2491,6 +2491,8 @@
(text "Create infinite Littleweb sites"))
(li
(text "Create infinite Littleweb domains"))
(li
(text "Create and sell CSS snippet products"))
(text "{% if config.security.enable_invite_codes -%}")
(li

View file

@ -150,35 +150,60 @@
(icon (text "package-check"))
(b
(str (text "economy:label.fulfillment_style"))))
(form
(div
("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."))
(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 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")))))
("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")
@ -347,6 +372,28 @@
});
}
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\", [
@ -378,7 +425,46 @@
}
}
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 %}")

View file

@ -19,26 +19,44 @@
("class" "card lowered w_full no_p_margin")
(text "{{ product.description|markdown|safe }}"))
(text "{% if already_purchased -%}")
(span
("class" "green flex items_center gap_2")
(icon (text "circle-check"))
(str (text "economy:label.already_purchased")))
(text "{%- endif %}")
(div
("class" "flex gap_2 items_center")
(text "{% if user.id != product.owner -%}")
(text "{% if not already_purchased -%}")
; price
(a
("class" "button camo lowered")
("href" "/wallet")
("target" "_blank")
(icon (text "badge-cent"))
(text "{{ product.price }}"))
(text "{% if user.id != product.owner -%}")
(text "{% if not already_purchased -%}")
; buy button
(button
("onclick" "purchase()")
("disabled" "{{ product.stock == 0 }}")
(icon (text "piggy-bank"))
(str (text "economy:action.buy")))
(text "{% else %}")
(span
("class" "green flex items_center gap_2")
(icon (text "circle-check"))
(str (text "economy:label.already_purchased")))
; profile style snippets
(text "{% if product.method == \"ProfileStyle\" -%} {% if not product.id in applied_configurations_mapped -%}")
(button
("onclick" "apply()")
(icon (text "check"))
(str (text "economy:action.apply")))
(text "{% else %}")
(button
("onclick" "remove()")
(icon (text "x"))
(str (text "economy:action.unapply")))
(text "{%- endif %} {%- endif %}")
; ...
(text "{%- endif %}")
(text "{% else %}")
(a
@ -69,6 +87,59 @@
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}
async function apply() {
await trigger(\"atto::debounce\", [\"user::update\"]);
fetch(\"/api/v1/auth/user/{{ user.id }}/applied_configuration\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
\"type\": \"StyleSnippet\",
\"id\": \"{{ product.id }}\",
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}
async function remove() {
await trigger(\"atto::debounce\", [\"user::update\"]);
fetch(\"/api/v1/auth/user/{{ user.id }}/applied_configuration\", {
method: \"DELETE\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
\"id\": \"{{ product.id }}\",
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
window.location.reload();
}
});
}"))
(text "{% endblock %}")

View file

@ -468,6 +468,12 @@
("class" "rhs w_full flex flex_col gap_4")
(text "{% block content %}{% endblock %}")))))
(text "{% if not use_user_theme -%}")
(text "{% for cnf in applied_configurations -%}")
(text "{{ cnf|safe }}")
(text "{%- endfor %}")
(text "{%- endif %}")
(text "{% if not is_self and profile.settings.warning -%}")
(script
(text "setTimeout(() => {

View file

@ -1162,6 +1162,26 @@
("class" "fade")
(text "This represents the site theme shown to users viewing
your profile.")))))
(text "{% if profile.applied_configurations|length > 0 -%}")
(div
("class" "card_nest")
("ui_ident" "applied_configurations")
(div
("class" "card small flex items_center gap_2")
(icon (text "cog"))
(str (text "setttings:label.applied_configurations")))
(div
("class" "card")
(p (text "Products that you have purchased and applied to your profile are displayed below. Snippets are always synced to the product, meaning the owner could update it at any time."))
(ul
(text "{% for cnf in profile.applied_configurations -%}")
(li
(text "{{ cnf[0] }} ")
(a
("href" "/product/{{ cnf[1] }}")
(text "{{ cnf[1] }}")))
(text "{%- endfor %}"))))
(text "{%- endif %}")
(button
("onclick" "save_settings()")
("id" "save_button")
@ -1742,6 +1762,7 @@
\"import_export\",
\"theme_preference\",
\"profile_theme\",
\"applied_configurations\",
]);
ui.generate_settings_ui(