new migration in apps.applets to seed wallet applet models; many expanded styles in wallet.js, chiefly concerned w. wallet-oriented FTs tbh; some intermittent Windows cache errors quashed in dash view ITs; apps.dash.views & .urls now support wallet applets; apps.lyric.models now discerns tithe coins (available for purchase soon); new styles across many scss files, again many concerning wallet applets but also applets more generally and also unorthodox media query parameters to make UX more usable; a slew of new wallet partials
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Disco DeDisco
2026-03-11 00:58:24 -04:00
parent 791510b46d
commit aa1cef6e7b
18 changed files with 441 additions and 83 deletions

View File

@@ -0,0 +1,37 @@
from django.db import migrations, models
def seed_wallet_applets(apps, schema_editor):
Applet = apps.get_model('applets', 'Applet')
for slug, name, cols, rows in [
('wallet-balances', 'Wallet Balances', 3, 3),
('wallet-tokens', 'Wallet Tokens', 3, 3),
('wallet-payment', 'Payment Methods', 6, 2),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={'name': name, 'grid_cols': cols, 'grid_rows': rows, 'context': 'wallet'},
)
class Migration(migrations.Migration):
dependencies = [
('applets', '0002_seed_applets'),
]
operations = [
migrations.AlterField(
model_name='applet',
name='context',
field=models.CharField(
choices=[
('dashboard', 'Dashboard'),
('gameboard', 'Gameboard'),
('wallet', 'Wallet'),
],
default='dashboard',
max_length=20,
),
),
migrations.RunPython(seed_wallet_applets, migrations.RunPython.noop),
]

View File

@@ -3,9 +3,11 @@ from django.db import models
class Applet(models.Model): class Applet(models.Model):
DASHBOARD = "dashboard" DASHBOARD = "dashboard"
GAMEBOARD = "gameboard" GAMEBOARD = "gameboard"
WALLET = "wallet"
CONTEXT_CHOICES = [ CONTEXT_CHOICES = [
(DASHBOARD, "Dashboard"), (DASHBOARD, "Dashboard"),
(GAMEBOARD, "Gameboard"), (GAMEBOARD, "Gameboard"),
(WALLET, "Wallet"),
] ]
slug = models.SlugField(unique=True) slug = models.SlugField(unique=True)

View File

@@ -3,6 +3,7 @@ const initWallet = () => {
const addBtn = document.getElementById('id_add_payment_method'); const addBtn = document.getElementById('id_add_payment_method');
const saveBtn = document.getElementById('id_save_payment_method'); const saveBtn = document.getElementById('id_save_payment_method');
const cancelBtn = document.getElementById('id_cancel_payment_method');
if (!addBtn) return; if (!addBtn) return;
const getCsrf = () => document.cookie.match(/csrftoken=([^;]+)/)[1]; const getCsrf = () => document.cookie.match(/csrftoken=([^;]+)/)[1];
@@ -15,8 +16,25 @@ const initWallet = () => {
const {client_secret, publishable_key} = await res.json(); const {client_secret, publishable_key} = await res.json();
stripe = Stripe(publishable_key); stripe = Stripe(publishable_key);
elements = stripe.elements({clientSecret: client_secret}); elements = stripe.elements({clientSecret: client_secret});
elements.create('payment').mount('#id_stripe_payment_element'); const paymentEl = elements.create('payment');
paymentEl.mount('#id_stripe_payment_element');
saveBtn.hidden = false; saveBtn.hidden = false;
cancelBtn.hidden = false;
const section = addBtn.closest('section');
const rowPx = 3 * parseFloat(getComputedStyle(document.documentElement).fontSize);
const updateRows = () => {
const rows = Math.ceil(section.scrollHeight / rowPx) + 1;
section.style.setProperty('--applet-rows', String(rows));
};
paymentEl.on('ready', () => {
updateRows();
const iframe = document.querySelector('#id_stripe_payment_element iframe');
if (iframe) {
const obs = new MutationObserver(updateRows);
obs.observe(iframe, { attributes: true, attributeFilter: ['style'] });
section._stripeObs = obs;
}
});
}); });
saveBtn.addEventListener('click', async () => { saveBtn.addEventListener('click', async () => {
@@ -37,7 +55,55 @@ const initWallet = () => {
const pm = document.createElement('div'); const pm = document.createElement('div');
pm.textContent = `${brand} ····${last4}`; pm.textContent = `${brand} ····${last4}`;
document.getElementById('id_payment_methods').appendChild(pm); document.getElementById('id_payment_methods').appendChild(pm);
elements.getElement('payment').unmount();
elements = null;
stripe = null;
saveBtn.hidden = true;
cancelBtn.hidden = true;
const section = cancelBtn.closest('section');
section.style.setProperty('--applet-rows', '2');
if (section._stripeObs) { section._stripeObs.disconnect(); section._stripeObs = null; }
});
cancelBtn.addEventListener('click', () => {
if (elements) {
elements.getElement('payment').unmount();
elements = null;
stripe = null;
}
saveBtn.hidden = true;
cancelBtn.hidden = true;
const section = cancelBtn.closest('section');
section.style.setProperty('--applet-rows', '2');
if (section._stripeObs) { section._stripeObs.disconnect(); section._stripeObs = null; }
}); });
}; };
function initWalletTooltips() {
const portal = document.getElementById('id_tooltip_portal');
if (!portal) return;
document.querySelectorAll('.wallet-tokens .token').forEach(token => {
const tooltip = token.querySelector('.token-tooltip');
if (!tooltip) return;
token.addEventListener('mouseenter', () => {
const rect = token.getBoundingClientRect();
portal.innerHTML = tooltip.innerHTML;
portal.classList.add('active');
const halfW = portal.offsetWidth / 2;
const rawLeft = rect.left + rect.width / 2;
const clampedLeft = Math.max(halfW + 8, Math.min(rawLeft, window.innerWidth - halfW - 8));
portal.style.left = Math.round(clampedLeft) + 'px';
portal.style.top = Math.round(rect.top) + 'px';
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
});
token.addEventListener('mouseleave', () => {
portal.classList.remove('active');
});
});
}
document.addEventListener('DOMContentLoaded', initWallet); document.addEventListener('DOMContentLoaded', initWallet);
document.addEventListener('DOMContentLoaded', initWalletTooltips);

View File

@@ -2,6 +2,7 @@ import lxml.html
from django.test import override_settings, TestCase from django.test import override_settings, TestCase
from apps.applets.models import Applet, UserApplet
from apps.lyric.models import Token, User, Wallet from apps.lyric.models import Token, User, Wallet
@@ -48,3 +49,94 @@ class WalletViewTest(TestCase):
self.assertGreater(len(bundles), 0) self.assertGreater(len(bundles), 0)
@override_settings(COMPRESS_ENABLED=False)
class WalletViewAppletContextTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="walletctx@test.io")
Applet.objects.get_or_create(
slug="wallet-balances",
defaults={"name": "Wallet Balances", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
)
Applet.objects.get_or_create(
slug="wallet-tokens",
defaults={"name": "Wallet Tokens", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
)
Applet.objects.get_or_create(
slug="wallet-payment",
defaults={"name": "Payment Methods", "grid_cols": 6, "grid_rows": 2, "context": "wallet"},
)
self.client.force_login(self.user)
def test_wallet_view_passes_applets_context(self):
response = self.client.get("/dashboard/wallet/")
slugs = [e["applet"].slug for e in response.context["applets"]]
self.assertIn("wallet-balances", slugs)
self.assertIn("wallet-tokens", slugs)
self.assertIn("wallet-payment", slugs)
def test_wallet_page_renders_applets_container(self):
response = self.client.get("/dashboard/wallet/")
parsed = lxml.html.fromstring(response.content)
[_] = parsed.cssselect("#id_wallet_applets_container")
def test_wallet_page_renders_gear_button(self):
response = self.client.get("/dashboard/wallet/")
parsed = lxml.html.fromstring(response.content)
[_] = parsed.cssselect(".gear-btn")
@override_settings(COMPRESS_ENABLED=False)
class ToggleWalletAppletsTest(TestCase):
def setUp(self):
self.user = User.objects.create(email="wallettoggle@test.io")
self.balances = Applet.objects.get_or_create(
slug="wallet-balances",
defaults={"name": "Wallet Balances", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
)[0]
self.tokens = Applet.objects.get_or_create(
slug="wallet-tokens",
defaults={"name": "Wallet Tokens", "grid_cols": 3, "grid_rows": 3, "context": "wallet"},
)[0]
Applet.objects.get_or_create(
slug="wallet-payment",
defaults={"name": "Payment Methods", "grid_cols": 6, "grid_rows": 2, "context": "wallet"},
)
self.client.force_login(self.user)
def test_toggle_requires_login(self):
self.client.logout()
response = self.client.post("/dashboard/wallet/toggle-applets", {})
self.assertRedirects(
response, "/?next=/dashboard/wallet/toggle-applets",
fetch_redirect_response=False,
)
def test_toggle_redirects_to_wallet(self):
response = self.client.post(
"/dashboard/wallet/toggle-applets", {"applets": ["wallet-balances"]}
)
self.assertRedirects(response, "/dashboard/wallet/", fetch_redirect_response=False)
def test_toggle_hides_unchecked_applet(self):
self.client.post(
"/dashboard/wallet/toggle-applets", {"applets": ["wallet-balances"]}
)
ua = UserApplet.objects.get(user=self.user, applet=self.tokens)
self.assertFalse(ua.visible)
def test_toggle_shows_checked_applet(self):
UserApplet.objects.create(user=self.user, applet=self.balances, visible=False)
self.client.post(
"/dashboard/wallet/toggle-applets", {"applets": ["wallet-balances"]}
)
ua = UserApplet.objects.get(user=self.user, applet=self.balances)
self.assertTrue(ua.visible)
def test_toggle_htmx_returns_container_partial(self):
response = self.client.post(
"/dashboard/wallet/toggle-applets",
{"applets": ["wallet-balances"]},
HTTP_HX_REQUEST="true",
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "id_wallet_applets_container")

View File

@@ -10,6 +10,7 @@ urlpatterns = [
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'), path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
path('toggle_applets', views.toggle_applets, name="toggle_applets"), path('toggle_applets', views.toggle_applets, name="toggle_applets"),
path('wallet/', views.wallet, name='wallet'), path('wallet/', views.wallet, name='wallet'),
path('wallet/toggle-applets', views.toggle_wallet_applets, name='toggle_wallet_applets'),
path('wallet/setup-intent', views.setup_intent, name='setup_intent'), path('wallet/setup-intent', views.setup_intent, name='setup_intent'),
path('wallet/save-payment-method', views.save_payment_method, name='save_payment_method'), path('wallet/save-payment-method', views.save_payment_method, name='save_payment_method'),
] ]

View File

@@ -144,15 +144,34 @@ def toggle_applets(request):
@login_required(login_url="/") @login_required(login_url="/")
@ensure_csrf_cookie @ensure_csrf_cookie
def wallet(request): def wallet(request):
wallet = request.user.wallet
coin = request.user.tokens.filter(token_type=Token.COIN).first()
free_tokens = list(request.user.tokens.filter(token_type=Token.FREE))
return render(request, "apps/dashboard/wallet.html", { return render(request, "apps/dashboard/wallet.html", {
"wallet": wallet, "wallet": request.user.wallet,
"coin": coin, "coin": request.user.tokens.filter(token_type=Token.COIN).first(),
"free_tokens": free_tokens, "free_tokens": list(request.user.tokens.filter(token_type=Token.FREE)),
"tithe_tokens": list(request.user.tokens.filter(token_type=Token.TITHE)),
"applets": applet_context(request.user, "wallet"),
"page_class": "page-wallet",
}) })
@login_required(login_url="/")
def toggle_wallet_applets(request):
checked = request.POST.getlist("applets")
for applet in Applet.objects.filter(context="wallet"):
UserApplet.objects.update_or_create(
user=request.user,
applet=applet,
defaults={"visible": applet.slug in checked},
)
if request.headers.get("HX-Request"):
return render(request, "apps/wallet/_partials/_applets.html", {
"applets": applet_context(request.user, "wallet"),
"wallet": request.user.wallet,
"coin": request.user.tokens.filter(token_type=Token.COIN).first(),
"free_tokens": list(request.user.tokens.filter(token_type=Token.FREE)),
"tithe_tokens": list(request.user.tokens.filter(token_type=Token.TITHE)),
})
return redirect("wallet")
@login_required(login_url="/") @login_required(login_url="/")
def setup_intent(request): def setup_intent(request):
stripe.api_key = settings.STRIPE_SECRET_KEY stripe.api_key = settings.STRIPE_SECRET_KEY

View File

@@ -86,6 +86,8 @@ class Token(models.Model):
def tooltip_description(self): def tooltip_description(self):
if self.token_type in (self.COIN, self.FREE): if self.token_type in (self.COIN, self.FREE):
return "Admit 1 Entry" return "Admit 1 Entry"
if self.token_type == self.TITHE:
return "+ Writ bonus"
return "" return ""
def tooltip_expiry(self): def tooltip_expiry(self):

View File

@@ -82,6 +82,16 @@ class FunctionalTest(StaticLiveServerTestCase):
def wait_for(self, fn): def wait_for(self, fn):
return fn() return fn()
def wait_for_slow(self, fn, timeout=30):
start_time = time.time()
while True:
try:
return fn()
except (AssertionError, WebDriverException) as e:
if time.time() - start_time > timeout:
raise e
time.sleep(0.5)
def create_pre_authenticated_session(self, email): def create_pre_authenticated_session(self, email):
if self.test_server: if self.test_server:
session_key = create_session_on_server(self.test_server, email) session_key = create_session_on_server(self.test_server, email)

View File

@@ -9,6 +9,15 @@ class WalletDisplayTest(FunctionalTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
Applet.objects.get_or_create(slug="wallet", defaults={"name": "Wallet"}) Applet.objects.get_or_create(slug="wallet", defaults={"name": "Wallet"})
for slug, name, cols, rows in [
("wallet-balances", "Wallet Balances", 3, 3),
("wallet-tokens", "Wallet Tokens", 3, 3),
("wallet-payment", "Payment Methods", 6, 2),
]:
Applet.objects.get_or_create(
slug=slug,
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "wallet"},
)
def test_new_user_wallet_shows_starting_balances(self): def test_new_user_wallet_shows_starting_balances(self):
# 1. Log in as new user # 1. Log in as new user
@@ -45,14 +54,10 @@ class WalletDisplayTest(FunctionalTest):
ActionChains(self.browser).move_to_element(coin).perform() ActionChains(self.browser).move_to_element(coin).perform()
self.wait_for( self.wait_for(
lambda: self.assertTrue( lambda: self.assertTrue(
self.browser.find_element( self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
By.CSS_SELECTOR, "#id_coin_on_a_string .token-tooltip"
).is_displayed()
) )
) )
coin_tooltip = self.browser.find_element( coin_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
By.CSS_SELECTOR, "#id_coin_on_a_string .token-tooltip"
).text
self.assertIn("Coin-on-a-String", coin_tooltip) self.assertIn("Coin-on-a-String", coin_tooltip)
self.assertIn("Admit 1 Entry", coin_tooltip) self.assertIn("Admit 1 Entry", coin_tooltip)
self.assertIn("no expiry", coin_tooltip) self.assertIn("no expiry", coin_tooltip)
@@ -62,14 +67,10 @@ class WalletDisplayTest(FunctionalTest):
ActionChains(self.browser).move_to_element(free_token).perform() ActionChains(self.browser).move_to_element(free_token).perform()
self.wait_for( self.wait_for(
lambda: self.assertTrue( lambda: self.assertTrue(
self.browser.find_element( self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
By.CSS_SELECTOR, "#id_free_token_0 .token-tooltip"
).is_displayed()
) )
) )
free_tooltip = self.browser.find_element( free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
By.CSS_SELECTOR, "#id_free_token_0 .token-tooltip"
).text
self.assertIn("Free Token", free_tooltip) self.assertIn("Free Token", free_tooltip)
self.assertIn("Admit 1 Entry", free_tooltip) self.assertIn("Admit 1 Entry", free_tooltip)
self.assertIn("Expires", free_tooltip) self.assertIn("Expires", free_tooltip)
@@ -125,10 +126,15 @@ class WalletDisplayTest(FunctionalTest):
).send_keys("42424") ).send_keys("42424")
# 6. Return to main doc & submit form # 6. Return to main doc & submit form
self.browser.switch_to.default_content() self.browser.switch_to.default_content()
self.wait_for(
lambda: self.assertFalse(
self.browser.find_element(By.ID, "id_save_payment_method").get_attribute("hidden")
)
)
self.browser.find_element(By.ID, "id_save_payment_method").click() self.browser.find_element(By.ID, "id_save_payment_method").click()
# 7. Wait for saved card to appear in payment methods list # 7. Wait for saved card to appear in payment methods list
# Assert last 4 digits shown # Assert last 4 digits shown (Stripe confirmSetup + server round-trip can be slow)
self.wait_for( self.wait_for_slow(
lambda: self.assertIn( lambda: self.assertIn(
"4242", "4242",
self.browser.find_element(By.ID, "id_payment_methods").text, self.browser.find_element(By.ID, "id_payment_methods").text,

View File

@@ -72,8 +72,9 @@
} }
} }
#id_dash_applet_menu { @extend %applet-menu; } #id_dash_applet_menu { @extend %applet-menu; }
#id_game_applet_menu { @extend %applet-menu; } #id_game_applet_menu { @extend %applet-menu; }
#id_wallet_applet_menu { @extend %applet-menu; }
// ── Applets grid (shared across all boards) ──────────────── // ── Applets grid (shared across all boards) ────────────────
%applets-grid { %applets-grid {
@@ -101,7 +102,7 @@
0.2rem solid rgba(var(--secUser), 0.5), 0.2rem solid rgba(var(--secUser), 0.5),
; ;
box-shadow: box-shadow:
inset -1px -1px 0 rgba(255, 255, 255, 0.125), inset -0.125rem -0.125rem 0 rgba(var(--ninUser), 0.125),
inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8) inset 0.125rem 0.125rem 0 rgba(0, 0, 0, 0.8)
; ;
border-radius: 0.75rem; border-radius: 0.75rem;
@@ -119,3 +120,4 @@
#id_applets_container { @extend %applets-grid; } #id_applets_container { @extend %applets-grid; }
#id_game_applets_container { @extend %applets-grid; } #id_game_applets_container { @extend %applets-grid; }
#id_wallet_applets_container { @extend %applets-grid; }

View File

@@ -193,7 +193,7 @@ body {
padding: 0.5rem 0; padding: 0.5rem 0;
.col-lg-6 h2 { .col-lg-6 h2 {
font-size: 2.1rem; font-size: 2rem;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
// text-justify: inter-character is Firefox-only; approximate for Safari/Chrome // text-justify: inter-character is Firefox-only; approximate for Safari/Chrome
letter-spacing: 1em; letter-spacing: 1em;
@@ -234,7 +234,7 @@ body {
text-align-last: center; text-align-last: center;
letter-spacing: 0.25em; letter-spacing: 0.25em;
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
font-size: 2.2rem; font-size: 2rem;
} }
} }
} }

View File

@@ -40,6 +40,10 @@ body.page-gameboard {
.gameboard-page { .gameboard-page {
min-width: 666px; min-width: 666px;
} }
body.page-gameboard .container {
overflow: visible;
}
} }
#id_applet_game_kit { #id_applet_game_kit {

View File

@@ -51,6 +51,58 @@
} }
} }
.token--empty {
cursor: help;
> i { opacity: 0.4; }
}
html:has(body.page-wallet) {
overflow: hidden;
}
body.page-wallet {
overflow: hidden;
.container {
overflow: hidden;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.row {
flex-shrink: 0;
}
}
.wallet-page {
position: relative;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.wallet-tokens {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
overflow: visible;
.token {
font-size: 1.5rem;
}
.token:hover .token-tooltip { display: none; }
}
#id_payment_methods {
overflow-y: auto;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.token .token-tooltip { .token .token-tooltip {
width: 13rem; width: 13rem;
@@ -58,4 +110,9 @@
left: 0; left: 0;
transform: none; transform: none;
} }
.wallet-tokens .token-tooltip {
left: 50%;
transform: translateX(-50%);
}
} }

View File

@@ -1,64 +1,15 @@
{% extends "core/base.html" %} {% extends "core/base.html" %}
{% load static %} {% load static %}
{% block title_text %}Dashboard{% endblock title_text %} {% block title_text %}Dashwallet{% endblock title_text %}
{% block header_text %}<span>Dash</span>board{% endblock header_text %} {% block header_text %}<span>Dash</span>wallet{% endblock header_text %}
{% block content %} {% block content %}
<div class="wallet-page"> <div class="wallet-page">
<h1>Wallet</h1> {% include "apps/applets/_partials/_gear.html" with menu_id="id_wallet_applet_menu" %}
{% include "apps/wallet/_partials/_applets.html" %}
<section class="wallet-balances">
<div><i class="fa-solid fa-ticket"></i>: <span id="id_writs_balance">{{ wallet.writs }}</span></div>
<div>Esteem: <span id="id_esteem_balance">{{ wallet.esteem }}</span></div>
</section>
<section class="wallet-tokens">
{% if coin %}
<div id="id_coin_on_a_string" class="token">
<i class="fa-solid fa-clover"></i>
<div class="token-tooltip">
<h4>{{ coin.tooltip_name }}</h4>
<p>{{ coin.tooltip_description }}</p>
{% if coin.tooltip_shoptalk %}
<small><em>{{ coin.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ coin.tooltip_expiry }}</p>
</div>
</div>
{% endif %}
{% for token in free_tokens %}
<div id="id_free_token_{{ forloop.counter0 }}" class="token">
<i class="fa-solid fa-coins"></i>
<div class="token-tooltip">
<h4>{{ token.tooltip_name }}</h4>
<p>{{ token.tooltip_description }}</p>
{% if token.tooltip_shoptalk %}
<small><em>{{ token.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ token.tooltip_expiry }}</p>
</div>
</div>
{% endfor %}
</section>
<section id="id_payment_methods">
<h2>Payment Methods</h2>
<button id="id_add_payment_method">Add Payment Method</button>
<div id="id_stripe_payment_element"></div>
<button id="id_save_payment_method" hidden>Save Card</button>
</section>
<section id="id_tithe_token_shop">
<h2>Tithe Tokens</h2>
<div class="token-bundle">
<span><i class="fa-solid fa-piggy-bank"></i> Tithe Token ×1</span>
<span>+ Writ bonus</span>
</div>
</section>
</div> </div>
<div id="id_tooltip_portal" class="token-tooltip"></div>
<script src="https://js.stripe.com/v3/"></script> <script src="https://js.stripe.com/v3/"></script>
<script src="{% static "apps/scripts/wallet.js" %}"></script> <script src="{% static "apps/scripts/wallet.js" %}"></script>
{% endblock content %} {% endblock content %}

View File

@@ -0,0 +1,19 @@
<section
id="id_wallet_balances"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
>
<div><i class="fa-solid fa-ticket"></i>: <span id="id_writs_balance">{{ wallet.writs }}</span></div>
<div>Esteem: <span id="id_esteem_balance">{{ wallet.esteem }}</span></div>
<div id="id_tithe_token_shop">
<div class="token-bundle" data-qty="1" data-price-cents="100">
<span class="bundle-qty">1 Tithe Token</span>
<span class="bundle-writs">+144 Writs</span>
<span class="bundle-price">$1.00</span>
</div>
<div class="token-bundle" data-qty="5" data-price-cents="400">
<span class="bundle-qty">5 Tithe Tokens</span>
<span class="bundle-writs">+750 Writs</span>
<span class="bundle-price">$4.00</span>
</div>
</div>
</section>

View File

@@ -0,0 +1,10 @@
<section
id="id_payment_methods"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
>
<h2>Payment Methods</h2>
<button id="id_add_payment_method">Add Payment Method</button>
<div id="id_stripe_payment_element"></div>
<button id="id_save_payment_method" hidden>Save Card</button>
<button id="id_cancel_payment_method" hidden>Cancel</button>
</section>

View File

@@ -0,0 +1,53 @@
<section
class="wallet-tokens"
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
>
{% if coin %}
<div id="id_coin_on_a_string" class="token">
<i class="fa-solid fa-clover"></i>
<div class="token-tooltip">
<h4>{{ coin.tooltip_name }}</h4>
<p>{{ coin.tooltip_description }}</p>
{% if coin.tooltip_shoptalk %}
<small><em>{{ coin.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ coin.tooltip_expiry }}</p>
</div>
</div>
{% endif %}
{% for token in free_tokens %}
<div id="id_free_token_{{ forloop.counter0 }}" class="token">
<i class="fa-solid fa-coins"></i>
<div class="token-tooltip">
<h4>{{ token.tooltip_name }}</h4>
<p>{{ token.tooltip_description }}</p>
{% if token.tooltip_shoptalk %}
<small><em>{{ token.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ token.tooltip_expiry }}</p>
</div>
</div>
{% endfor %}
{% for token in tithe_tokens %}
<div id="id_tithe_token_{{ forloop.counter0 }}" class="token">
<i class="fa-solid fa-piggy-bank"></i>
<div class="token-tooltip">
<h4>{{ token.tooltip_name }}</h4>
<p>{{ token.tooltip_description }}</p>
{% if token.tooltip_shoptalk %}
<small><em>{{ token.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ token.tooltip_expiry }}</p>
</div>
</div>
{% empty %}
<div id="id_tithe_token_empty" class="token token--empty">
<i class="fa-solid fa-piggy-bank"></i>
<div class="token-tooltip">
<h4>Tithe Token</h4>
<p>0 owned</p>
<p class="expiry">purchase one above</p>
</div>
</div>
{% endfor %}
</section>

View File

@@ -0,0 +1,27 @@
<div id="id_wallet_applets_container">
<div id="id_wallet_applet_menu" style="display:none;">
<form
hx-post="{% url "toggle_wallet_applets" %}"
hx-target="#id_wallet_applets_container"
hx-swap="outerHTML"
>
{% csrf_token %}
{% for entry in applets %}
<label>
<input
type="checkbox"
name="applets"
value="{{ entry.applet.slug }}"
{% if entry.visible %}checked{% endif %}
>
{{ entry.applet.name }}
</label>
{% endfor %}
<div class="menu-btns">
<button type="submit" class="btn btn-confirm">OK</button>
<button type="button" class="btn btn-cancel applet-menu-cancel">NVM</button>
</div>
</form>
</div>
{% include "apps/applets/_partials/_applets.html" %}
</div>