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
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
37
src/apps/applets/migrations/0003_wallet_applets.py
Normal file
37
src/apps/applets/migrations/0003_wallet_applets.py
Normal 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),
|
||||
]
|
||||
@@ -3,9 +3,11 @@ from django.db import models
|
||||
class Applet(models.Model):
|
||||
DASHBOARD = "dashboard"
|
||||
GAMEBOARD = "gameboard"
|
||||
WALLET = "wallet"
|
||||
CONTEXT_CHOICES = [
|
||||
(DASHBOARD, "Dashboard"),
|
||||
(GAMEBOARD, "Gameboard"),
|
||||
(WALLET, "Wallet"),
|
||||
]
|
||||
|
||||
slug = models.SlugField(unique=True)
|
||||
|
||||
@@ -3,6 +3,7 @@ const initWallet = () => {
|
||||
|
||||
const addBtn = document.getElementById('id_add_payment_method');
|
||||
const saveBtn = document.getElementById('id_save_payment_method');
|
||||
const cancelBtn = document.getElementById('id_cancel_payment_method');
|
||||
if (!addBtn) return;
|
||||
|
||||
const getCsrf = () => document.cookie.match(/csrftoken=([^;]+)/)[1];
|
||||
@@ -15,8 +16,25 @@ const initWallet = () => {
|
||||
const {client_secret, publishable_key} = await res.json();
|
||||
stripe = Stripe(publishable_key);
|
||||
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;
|
||||
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 () => {
|
||||
@@ -37,7 +55,55 @@ const initWallet = () => {
|
||||
const pm = document.createElement('div');
|
||||
pm.textContent = `${brand} ····${last4}`;
|
||||
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', initWalletTooltips);
|
||||
@@ -2,6 +2,7 @@ import lxml.html
|
||||
|
||||
from django.test import override_settings, TestCase
|
||||
|
||||
from apps.applets.models import Applet, UserApplet
|
||||
from apps.lyric.models import Token, User, Wallet
|
||||
|
||||
|
||||
@@ -48,3 +49,94 @@ class WalletViewTest(TestCase):
|
||||
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")
|
||||
|
||||
@@ -10,6 +10,7 @@ urlpatterns = [
|
||||
path('users/<uuid:user_id>/', views.my_lists, name='my_lists'),
|
||||
path('toggle_applets', views.toggle_applets, name="toggle_applets"),
|
||||
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/save-payment-method', views.save_payment_method, name='save_payment_method'),
|
||||
]
|
||||
|
||||
@@ -144,15 +144,34 @@ def toggle_applets(request):
|
||||
@login_required(login_url="/")
|
||||
@ensure_csrf_cookie
|
||||
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", {
|
||||
"wallet": wallet,
|
||||
"coin": coin,
|
||||
"free_tokens": free_tokens,
|
||||
"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)),
|
||||
"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="/")
|
||||
def setup_intent(request):
|
||||
stripe.api_key = settings.STRIPE_SECRET_KEY
|
||||
|
||||
@@ -86,6 +86,8 @@ class Token(models.Model):
|
||||
def tooltip_description(self):
|
||||
if self.token_type in (self.COIN, self.FREE):
|
||||
return "Admit 1 Entry"
|
||||
if self.token_type == self.TITHE:
|
||||
return "+ Writ bonus"
|
||||
return ""
|
||||
|
||||
def tooltip_expiry(self):
|
||||
|
||||
@@ -82,6 +82,16 @@ class FunctionalTest(StaticLiveServerTestCase):
|
||||
def wait_for(self, 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):
|
||||
if self.test_server:
|
||||
session_key = create_session_on_server(self.test_server, email)
|
||||
|
||||
@@ -9,6 +9,15 @@ class WalletDisplayTest(FunctionalTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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):
|
||||
# 1. Log in as new user
|
||||
@@ -45,14 +54,10 @@ class WalletDisplayTest(FunctionalTest):
|
||||
ActionChains(self.browser).move_to_element(coin).perform()
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_coin_on_a_string .token-tooltip"
|
||||
).is_displayed()
|
||||
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||
)
|
||||
)
|
||||
coin_tooltip = self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_coin_on_a_string .token-tooltip"
|
||||
).text
|
||||
coin_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
|
||||
self.assertIn("Coin-on-a-String", coin_tooltip)
|
||||
self.assertIn("Admit 1 Entry", coin_tooltip)
|
||||
self.assertIn("no expiry", coin_tooltip)
|
||||
@@ -62,14 +67,10 @@ class WalletDisplayTest(FunctionalTest):
|
||||
ActionChains(self.browser).move_to_element(free_token).perform()
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_free_token_0 .token-tooltip"
|
||||
).is_displayed()
|
||||
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
|
||||
)
|
||||
)
|
||||
free_tooltip = self.browser.find_element(
|
||||
By.CSS_SELECTOR, "#id_free_token_0 .token-tooltip"
|
||||
).text
|
||||
free_tooltip = self.browser.find_element(By.ID, "id_tooltip_portal").text
|
||||
self.assertIn("Free Token", free_tooltip)
|
||||
self.assertIn("Admit 1 Entry", free_tooltip)
|
||||
self.assertIn("Expires", free_tooltip)
|
||||
@@ -125,10 +126,15 @@ class WalletDisplayTest(FunctionalTest):
|
||||
).send_keys("42424")
|
||||
# 6. Return to main doc & submit form
|
||||
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()
|
||||
# 7. Wait for saved card to appear in payment methods list
|
||||
# Assert last 4 digits shown
|
||||
self.wait_for(
|
||||
# Assert last 4 digits shown (Stripe confirmSetup + server round-trip can be slow)
|
||||
self.wait_for_slow(
|
||||
lambda: self.assertIn(
|
||||
"4242",
|
||||
self.browser.find_element(By.ID, "id_payment_methods").text,
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
|
||||
#id_dash_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 {
|
||||
@@ -101,7 +102,7 @@
|
||||
0.2rem solid rgba(var(--secUser), 0.5),
|
||||
;
|
||||
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)
|
||||
;
|
||||
border-radius: 0.75rem;
|
||||
@@ -119,3 +120,4 @@
|
||||
|
||||
#id_applets_container { @extend %applets-grid; }
|
||||
#id_game_applets_container { @extend %applets-grid; }
|
||||
#id_wallet_applets_container { @extend %applets-grid; }
|
||||
|
||||
@@ -193,7 +193,7 @@ body {
|
||||
padding: 0.5rem 0;
|
||||
|
||||
.col-lg-6 h2 {
|
||||
font-size: 2.1rem;
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
// text-justify: inter-character is Firefox-only; approximate for Safari/Chrome
|
||||
letter-spacing: 1em;
|
||||
@@ -234,7 +234,7 @@ body {
|
||||
text-align-last: center;
|
||||
letter-spacing: 0.25em;
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 2.2rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,10 @@ body.page-gameboard {
|
||||
.gameboard-page {
|
||||
min-width: 666px;
|
||||
}
|
||||
|
||||
body.page-gameboard .container {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
#id_applet_game_kit {
|
||||
|
||||
@@ -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) {
|
||||
.token .token-tooltip {
|
||||
width: 13rem;
|
||||
@@ -58,4 +110,9 @@
|
||||
left: 0;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.wallet-tokens .token-tooltip {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,15 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title_text %}Dashboard{% endblock title_text %}
|
||||
{% block header_text %}<span>Dash</span>board{% endblock header_text %}
|
||||
{% block title_text %}Dashwallet{% endblock title_text %}
|
||||
{% block header_text %}<span>Dash</span>wallet{% endblock header_text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wallet-page">
|
||||
<h1>Wallet</h1>
|
||||
|
||||
<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>
|
||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_wallet_applet_menu" %}
|
||||
{% include "apps/wallet/_partials/_applets.html" %}
|
||||
</div>
|
||||
<div id="id_tooltip_portal" class="token-tooltip"></div>
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<script src="{% static "apps/scripts/wallet.js" %}"></script>
|
||||
{% endblock content %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
27
src/templates/apps/wallet/_partials/_applets.html
Normal file
27
src/templates/apps/wallet/_partials/_applets.html
Normal 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>
|
||||
Reference in New Issue
Block a user