add: user ads

This commit is contained in:
trisua 2025-08-11 20:21:05 -04:00
parent 46b3e66cd4
commit 2cb7d08ddc
41 changed files with 1095 additions and 29 deletions

View file

@ -1,5 +1,10 @@
(div ("id" "toast_zone"))
; ads
(script ("src" "/js/ads.js?v=tetratto-{{ random_cache_breaker }}"))
(script
(text "TetrattoAds.init();"))
; large text
(text "{% if user and user.settings.large_text -%}")
(style
@ -76,6 +81,8 @@
return;
}
TetrattoAds.render_ads(\"{{ config.system_user }}\", \"\");
atto.disconnect_observers();
atto.remove_false_options();
atto.clean_date_codes();

View file

@ -182,7 +182,7 @@
(text "{%- endif %} {%- endif %}")
(button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))))
(str (text "communities:action.create")))))))
(text "{% if not quoting -%}")
(script
(text "async function create_post_from_form(e) {

View file

@ -39,7 +39,7 @@
(span
(text "Make this a forum community")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}")
(div
("class" "card_nest w_full")

View file

@ -302,7 +302,7 @@
("minlength" "2")
("maxlength" "32")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(text "{% for channel in channels %}")
(div
("class" "card_nest")
@ -472,7 +472,7 @@
("accept" "image/png,image/jpeg,image/avif,image/webp")
("class" "w_full")))
(button
(text "{{ text \"communities:action.create\" }}"))
(str (text "communities:action.create")))
(span
("class" "fade")
(text "Emojis can be a maximum of 256 KiB."))))
@ -646,7 +646,7 @@
("min" "0")
("max" "256")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(text "{% for id, topic in community.topics %}")
(div
("class" "card_nest")

View file

@ -894,7 +894,7 @@
(div
("class" "flex gap_2")
(button
(text "{{ text \"communities:action.create\" }}"))
(str (text "communities:action.create")))
(text "{% if drawing_enabled -%}")
(button
@ -2494,6 +2494,11 @@
(li
(text "Create and sell CSS snippet products"))
(text "{% if config.enable_user_ads -%}")
(li
(text "No ads"))
(text "{%- endif %}")
(text "{% if config.security.enable_invite_codes -%}")
(li
(text "Create up to 48 invite codes")
@ -2709,3 +2714,33 @@
(icon (text "badge-cent"))
(text "{{ product.price }}")))
(text "{%- endmacro %}")
(text "{% macro ad_listing_card(ad) -%}")
(a
("class" "card button lowered w_full flex flex_col gap_2")
("href" "/product/ad/{{ ad.id }}/edit")
(b
("class" "flex gap_2 items_center")
("style" "height: 24px; text-decoration: {% if not ad.is_running -%} line-through {%- else -%} none {%- endif %}")
(icon (text "link"))
(text "{{ ad.target }}"))
(b
("style" "height: 18px")
(text "{% if ad.is_running -%}")
(span
("class" "green flex gap_2 items_center")
(icon (text "circle-check"))
(text "Running"))
(text "{% else %}")
(span
("class" "red flex gap_2 items_center")
(icon (text "circle-x"))
(text "Not running"))
(text "{%- endif %}")))
(text "{%- endmacro %}")
(text "{% macro advertisement(size=\"Leaderboard\") -%}")
(text "{% if not is_supporter and config.enable_user_ads -%}")
(object ("class" "tetratto_ad") ("data-ad-size" "{{ size }}"))
(text "{%- endif %}")
(text "{%- endmacro %}")

View file

@ -54,7 +54,7 @@
("placeholder" "redirect URL")
("minlength" "2")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
; app listing
(div

View file

@ -0,0 +1,62 @@
(text "<!doctype html>")
(html
("lang" "en")
(head
(meta ("charset" "UTF-8"))
(meta ("name" "viewport") ("content" "width=device-width, initial-scale=1.0"))
(meta ("http-equiv" "X-UA-Compatible") ("content" "ie=edge")))
(body
(a
("href" "{% if not disable_click -%} {{ config.host }}/api/v1/ads/host/{{ host }}/{{ ad.id }}/click {%- endif %}")
("title" "Advertisement")
("target" "_blank")
("class" "ad"))
(span ("class" "display_tag") (text "Ad"))
(style
(text "* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
line-height: 1.5;
letter-spacing: 0.15px;
font-family:
\"Inter\", \"Poppins\", \"Roboto\", ui-sans-serif, system-ui, sans-serif,
\"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\",
\"Noto Color Emoji\";
}
body {
overflow: hidden;
display: grid;
place-items: center;
}
a.ad {
display: inline;
width: 100dvw;
height: 100dvh;
background-image: url(\"{{ config.host|safe }}/api/v1/uploads/{{ ad.upload_id }}\");
background-position: center;
background-size: contain;
}
.display_tag {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
padding: 0.15rem 0.5rem;
background: hsla(0, 0%, 0%, 50%);
color: white;
font-weight: 600;
font-size: 10px;
user-select: none;
pointer-events: none;
border-radius: 0.4rem;
box-shadow: 0 0 2px hsla(0, 0%, 0%, 25%);
}"))))

View file

@ -0,0 +1,97 @@
(text "{% extends \"root.html\" %} {% block head %}")
(title
(text "Manage advertisement - {{ 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 "link"))
(b
(text "{{ ad.target }}")))
(form
("class" "card flex flex_col gap_2")
("onsubmit" "event.preventDefault(); update_is_running_from_form(event.target.is_running.checked)")
(object ("class" "tetratto_ad") ("data-ad-size" "{{ ad.size }}") ("data-noclick" "true") ("data-ad-id" "{{ ad.id }}"))
(ul
(li
(text "{% if ad.last_charge_time != 0 -%}")
(text "Last charge: ") (span ("class" "date") (text "{{ ad.last_charge_time }}"))
(text "{% else %}")
(text "No previous charges")
(text "{%- endif %}")))
(div ("class" "squig"))
(p (text "Each day your ad is viewed, you'll be charged 25 coins. This charge only applies to the very first view of the day."))
(p (text "Additionally, you'll be charged 2 coins per click on your ad. This fee will be paid to the user which hosts the site your ad was shown on."))
(p (text "Each of these transfers will be shown in your wallet's transfer table as either \"AdClick\" or \"AdCharge\"."))
(label
("for" "is_running")
("class" "flex items_center gap_2")
(input
("type" "checkbox")
("id" "is_running")
("name" "is_running")
("class" "w_content")
("checked" "{{ ad.is_running }}"))
(span
(str (text "economy:label.running"))))
(button (str (text "general:action.save")))))
(div
("class" "flex gap_2")
(a
("class" "button secondary")
("href" "/products")
(icon (text "arrow-left"))
(str (text "general:action.back")))
(button
("class" "lowered red")
("onclick" "delete_ad()")
(icon (text "trash"))
(str (text "general:action.delete")))))
(script
(text "async function update_is_running_from_form(is_running) {
await trigger(\"atto::debounce\", [\"products::update\"]);
fetch(\"/api/v1/ads/{{ ad.id }}/running\", {
method: \"POST\",
headers: {
\"Content-Type\": \"application/json\",
},
body: JSON.stringify({
is_running,
}),
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
}
async function delete_ad() {
if (
!(await trigger(\"atto::confirm\", [
\"Are you sure you would like to do this?\",
]))
) {
return;
}
fetch(\"/api/v1/ads/{{ ad.id }}\", {
method: \"DELETE\",
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
});
};"))
(text "{% endblock %}")

View file

@ -43,7 +43,7 @@
("minlength" "2")
("maxlength" "1024")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
; product listing
(div
@ -56,7 +56,105 @@
(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) }}"))))
; selective pagination
(text "{% if page_set_id == 0 -%}")
(text "{{ components::pagination(page=page, items=list|length) }}")
(text "{% else %}")
(text "{{ components::pagination(page=0, items=list|length) }}")
(text "{%- endif %}")))
(text "{% if config.enable_user_ads -%}")
(div ("class" "squig") ("style" "--background: var(--color-surface)"))
; create new ad
(div
("class" "card_nest")
(div
("class" "card small")
(b
(str (text "economy:label.create_new_ad"))))
(form
("class" "card flex flex_col gap_2")
("onsubmit" "create_ad_from_form(event)")
(div
("class" "flex flex_col gap_1")
(label
("for" "target")
(str (text "economy:label.target")))
(input
("type" "url")
("name" "target")
("id" "target")
("placeholder" "target url")
("required" "")
("minlength" "2")
("maxlength" "128")))
(div
("class" "flex flex_col gap_1")
(label
("for" "file")
(str (text "economy:label.image")))
(input
("id" "file")
("name" "file")
("type" "file")
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
("required" "")
("class" "w_content")))
(div
("class" "flex flex_col gap_1")
(label
("for" "size_base")
(str (text "economy:label.size_base")))
(select
("id" "size_base")
("name" "size_base")
(option ("value" "Leaderboard") (text "Leaderboard (720x90)"))
(option ("value" "Billboard") (text "Billboard (970x250)"))
(option ("value" "Skyscraper") (text "Skyscraper (160x600)"))
(option ("value" "MediumRectangle") (text "Medium rectangle (300x250)"))
(option ("value" "MobileLeaderboard") (text "Mobile leaderboard (320x50, mobile only)"))))
(button
(str (text "communities:action.create")))))
; ad listing
(div
("class" "card_nest")
(div
("class" "card small flex items_center gap_2")
(icon (text "images"))
(str (text "economy:label.my_ads")))
(div
("class" "card flex flex_col gap_2")
(text "{% for item in ads_list %} {{ components::ad_listing_card(ad=item) }} {% endfor %}")
; selective pagination
(text "{% if page_set_id == 1 -%}")
(text "{{ components::pagination(page=page, items=ads_list|length, key=\"&page_set_id=1\") }}")
(text "{% else %}")
(text "{{ components::pagination(page=0, items=ads_list|length, key=\"&page_set_id=1\") }}")
(text "{%- endif %}")))
(div
("class" "card_nest")
(div
("class" "card small flex items_center gap_2")
(icon (text "code"))
(str (text "economy:label.embed_ads_on_my_site")))
(div
("class" "card flex flex_col gap_2")
(p (text "You can embed the advertising network into your site to earn a (coin) commission from clicks."))
(p (text "Place the following into your site's HTML:"))
(pre (code (text "&lt;script src=\"{{ config.host }}\"/js/ads.js\"&gt;&lt;/script&gt;
&lt;script&gt;TetrattoAds.init&lpar;&rpar;; TetrattoAds.render_ads&lpar;\"{{ user.id }}\", \"{{ config.host }}\"&rpar;&lt;/script&gt;")))
(p (text "After you've done that, you can place your ads like so:"))
(pre (code (text "&lt;object class=\"tetratto_ad\" data-ad-size=\"$size$\"&gt;&lt;/object&gt;")))
(p
(text "In the above example, replace \"$size$\" with a size from ")
(a ("href" "https://tetratto.com/reference/tetratto/model/economy/enum.UserAdSize.html") (text "here"))
(text " keep in mind that the name of a size must be in title case. That means it's \"Leaderboard\", not \"leaderboard\"."))))
(text "{%- endif %}"))
(script
(text "async function create_product_from_form(e) {
@ -87,5 +185,43 @@
}, 100);
}
});
}
async function create_ad_from_form(e) {
e.preventDefault();
await trigger(\"atto::debounce\", [\"products::create\"]);
// create body
const body = new FormData();
for (const file of e.target.file.files) {
body.append(file.name, file);
}
body.append(
\"body\",
JSON.stringify({
target: e.target.target.value,
size: e.target.size_base.selectedOptions[0].value,
}),
);
// ...
fetch(\"/api/v1/ads\", {
method: \"POST\",
body,
})
.then((res) => res.json())
.then((res) => {
trigger(\"atto::toast\", [
res.ok ? \"success\" : \"error\",
res.message,
]);
if (res.ok) {
e.target.reset();
window.location.reload();
}
});
}"))
(text "{% endblock %}")

View file

@ -72,7 +72,7 @@
(text "{%- endif %}"))
(td (text "{{ transfer[3].source }}"))
(td
(text "{% if user.id == transfer[1].id -%}")
(text "{% if user.id == transfer[1].id and transfer[3].source == '\"Sale\"' -%}")
; we're the receiver
(button
("class" "small tiny square raised camo big_icon")

View file

@ -30,7 +30,7 @@
("minlength" "2")
("maxlength" "32")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(text "{% else %}")
(text "{{ components::developer_pass_ad(body=\"Get a developer pass to create forges!\") }}")
(text "{%- endif %}")

View file

@ -59,7 +59,7 @@
(option ("value" "{{ tld }}") (text ".{{ tld|lower }}"))
(text "{%- endfor %}")))
(button
(text "{{ text \"communities:action.create\" }}"))
(str (text "communities:action.create")))
(details
(summary

View file

@ -45,7 +45,7 @@
("minlength" "2")
("maxlength" "32")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(text "{%- endif %}")
(div
("class" "card_nest w_full")

View file

@ -28,7 +28,7 @@
("required" "")
("minlength" "16")))
(button
(text "{{ text \"communities:action.create\" }}")))))
(str (text "communities:action.create"))))))
(script
(text "function create_report_from_form(e) {

View file

@ -19,7 +19,7 @@
("class" "lowered small")
(text "{{ icon \"plus\" }}")
(span
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(div
("class" "card flex flex_col gap_2")
(text "{% for item in items %}")

View file

@ -37,7 +37,7 @@
("minlength" "2")
("maxlength" "4096")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(div
("class" "card_nest")
(div

View file

@ -88,7 +88,7 @@
("class" "flex gap_2")
(text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
(button
(text "{{ text \"communities:action.create\" }}")))))
(str (text "communities:action.create"))))))
(text "{%- endif %}")
(div
("class" "pillmenu")

View file

@ -29,7 +29,7 @@
("minlength" "2")
("maxlength" "32")))
(button
(text "{{ text \"communities:action.create\" }}"))))
(str (text "communities:action.create")))))
(text "{%- endif %}")
(div
("class" "card_nest w_full")

View file

@ -4,7 +4,9 @@
(text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main
("class" "flex flex_col gap_2")
(text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", forum_posts=\"/all/forum_posts\") }} {% if not user -%}")
(text "{{ macros::timelines_nav(selected=\"all\", posts=\"/all\", questions=\"/all/questions\", forum_posts=\"/all/forum_posts\") }}")
(text "{{ components::advertisement(size=\"Leaderboard\") }}")
(text "{% if not user -%}")
(div
("class" "card_nest")
(div