feat: wallet Shop applet — tile grid + BUY-ITEM microbutton + Stripe.js wiring — Chunk 4 of [[project-wallet-shop-expansion]]. The shop applet (slug wallet-shop, seeded in Chunk 2) now renders the catalog as a horizontal grid of .shop-tile icons: tithe-1 ($1, fa-piggy-bank), tithe-5 ($4, fa-piggy-bank w. ×5 badge), band-1 ($20, fa-ring). Each tile hosts a hover-portaled tooltip carrying name + description + price + a .tt-microbutton-portal w. a .btn-primary BUY ITEM button — clicking opens #id_guard_portal w. "Buy {name} for ${price}?" prompt; confirming triggers Stripe.js confirmCardPayment then POSTs to /shop/confirm + reloads. Items where the user's owned-count has hit max_owned (eg. BAND, owned=1, cap=1) render w. .btn-disabled + × glyph + "Already owned" microtooltip text — visible-but-unbuyable per the locked decision. View context — wallet view + toggle_wallet_applets view both pass shop_items (decorated w. per-user .available via the new _shop_items_for(user) helper) + default_payment_method_id + stripe_publishable_key. SCSS — .wallet-shop (flex column wrapping .shop-grid flex row), .shop-tile (inline-flex tooltip target), .shop-badge (2rem circle, --quaUser glyph on --quiUser bg, top-right corner per spec), .tt-microbutton-portal (column-flex, BUY btn + 'Already owned' caption styling). JS in wallet-shop.js exposes a singleton WalletShop module (matching the project's Brief / SeaDeal / StageCard module pattern) w. a tested initWalletShop() method — uses event delegation on the shop root (so portal-relocated buy btns still hit the handler) + a DOM-keyed data-shop-wired flag (not a module-level boolean) so per-test fixture rebuilds re-wire cleanly. Wired into wallet.html after wallet.js. **TDD** — 5 Jasmine specs in WalletShopSpec.js: T1 click-on-enabled-BUY opens guard w. correct prompt; T2 click-on-disabled-BUY no-op; T3 onConfirm POSTs shop_item_slug to /shop/buy; T4 init idempotent (calling twice doesn't double-wire); T5 missing-root no-throw. **2 Jasmine traps caught**: (a) spyOn(window, 'fetch') collides if another spec already spied on fetch — switched to save+restore via per-test _origFetch capture; (b) T3 async pollution — sync assertion passed, afterEach restored window.Stripe=undefined, then _doBuy's async continuation hit Stripe(pubKey) and threw "Unhandled promise rejection". Fixed by T3-local fetch mock returning a never-resolving promise so the chain pauses at the first await. **3 new FTs** in test_dash_wallet.py: tiles + icons + ×5 badge + tooltip prose; BUY click opens guard portal + NVM dismisses; BAND-already-owned shows disabled BUY w. 'Already owned' microtext (reads via textContent since .tt is display: none). FT trap caught: TransactionTestCase wipes both migration-seeded Applets + ShopItems → setUp must re-seed both manually (mirrors test_shop_views.py's _seed_starting_items pattern). 1208 IT/UT + 9 wallet FTs + 5 Jasmine specs green
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,4 +12,5 @@
|
||||
<div id="id_tooltip_portal" class="token-tooltip"></div>
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<script src="{% static "apps/dashboard/wallet.js" %}"></script>
|
||||
<script src="{% static "apps/dashboard/wallet-shop.js" %}"></script>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -2,13 +2,46 @@
|
||||
id="id_wallet_shop"
|
||||
class="wallet-shop"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
data-default-payment-method-id="{{ default_payment_method_id }}"
|
||||
data-stripe-publishable-key="{{ stripe_publishable_key }}"
|
||||
>
|
||||
{% comment %}
|
||||
Stub. Chunk 2 of the wallet-expansion sprint ships the row + model
|
||||
catalog; Chunk 4 fills in shop-tile rendering + BUY-ITEM microtooltip
|
||||
+ Stripe.js wiring. Keep the <section> element + id_wallet_shop hook
|
||||
so any test that just checks for shop visibility passes from Chunk 2
|
||||
onward (TDD's "ship the skeleton, fill the body next" pattern).
|
||||
{% endcomment %}
|
||||
<h2>Shop</h2>
|
||||
<div class="shop-grid">
|
||||
{% for item in shop_items %}
|
||||
<div
|
||||
id="id_shop_{{ item.slug }}"
|
||||
class="shop-tile"
|
||||
data-shop-item-slug="{{ item.slug }}"
|
||||
data-price-cents="{{ item.price_cents }}"
|
||||
data-item-name="{{ item.name }}"
|
||||
>
|
||||
<i class="fa-solid {{ item.icon }}"></i>
|
||||
{% if item.badge_text %}
|
||||
<span class="shop-badge">{{ item.badge_text }}</span>
|
||||
{% endif %}
|
||||
<div class="tt">
|
||||
<h4 class="tt-title">{{ item.name }}</h4>
|
||||
<p class="tt-description">{{ item.description }}</p>
|
||||
<p class="tt-price">{{ item.price_display }}</p>
|
||||
<div class="tt-microbutton-portal">
|
||||
{% if item.available %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary tt-buy-btn"
|
||||
data-shop-item-slug="{{ item.slug }}"
|
||||
>BUY ITEM</button>
|
||||
{% else %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary tt-buy-btn btn-disabled"
|
||||
data-shop-item-slug="{{ item.slug }}"
|
||||
aria-disabled="true"
|
||||
>×</button>
|
||||
<p class="tt-already-owned"><em>Already owned</em></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user