game kit page: four 6×3 applets (trinkets, tokens, card decks, dice sets) with applet grid; tarot fan modal with coverflow, sessionStorage position memory, and 403 guard on locked decks; unlocked_decks M2M on User with backfill migration; game kit icon wrap fix; tarot_deck.html moved to gameboard/ per template dir convention (now documented in CLAUDE.md); FTs 6–13, 2 new ITs; 360 passing [log Co-Authored-By: Claude Sonnet 4.6]
This commit is contained in:
106
src/apps/gameboard/static/apps/gameboard/game-kit.js
Normal file
106
src/apps/gameboard/static/apps/gameboard/game-kit.js
Normal file
@@ -0,0 +1,106 @@
|
||||
function initGameKitPage() {
|
||||
const dialog = document.getElementById('id_tarot_fan_dialog');
|
||||
if (!dialog) return;
|
||||
|
||||
const fanContent = document.getElementById('id_fan_content');
|
||||
const prevBtn = document.getElementById('id_fan_prev');
|
||||
const nextBtn = document.getElementById('id_fan_next');
|
||||
|
||||
let currentDeckId = null;
|
||||
let currentIndex = 0;
|
||||
let cards = [];
|
||||
|
||||
function storageKey(deckId) {
|
||||
return 'tarot-fan-' + deckId;
|
||||
}
|
||||
|
||||
function savePosition() {
|
||||
if (currentDeckId !== null) {
|
||||
sessionStorage.setItem(storageKey(currentDeckId), currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function restorePosition(deckId) {
|
||||
const saved = sessionStorage.getItem(storageKey(deckId));
|
||||
return saved !== null ? parseInt(saved, 10) : 0;
|
||||
}
|
||||
|
||||
function cardTransform(offset) {
|
||||
const abs = Math.abs(offset);
|
||||
return {
|
||||
transform: 'translateX(' + (offset * 200) + 'px) rotateY(' + (offset * 22) + 'deg) scale(' + Math.max(0.3, 1 - abs * 0.15) + ')',
|
||||
opacity: Math.max(0.15, 1 - abs * 0.25),
|
||||
zIndex: 10 - abs,
|
||||
};
|
||||
}
|
||||
|
||||
function updateFan() {
|
||||
const total = cards.length;
|
||||
if (!total) return;
|
||||
cards.forEach(function(card, i) {
|
||||
let offset = i - currentIndex;
|
||||
if (offset > total / 2) offset -= total;
|
||||
if (offset < -total / 2) offset += total;
|
||||
|
||||
const abs = Math.abs(offset);
|
||||
card.classList.toggle('fan-card--active', offset === 0);
|
||||
|
||||
if (abs > 3) {
|
||||
card.style.display = 'none';
|
||||
} else {
|
||||
card.style.display = '';
|
||||
const t = cardTransform(offset);
|
||||
card.style.transform = t.transform;
|
||||
card.style.opacity = t.opacity;
|
||||
card.style.zIndex = t.zIndex;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openFan(deckId) {
|
||||
currentDeckId = deckId;
|
||||
currentIndex = restorePosition(deckId);
|
||||
|
||||
fetch('/gameboard/game-kit/deck/' + deckId + '/')
|
||||
.then(function(r) { return r.text(); })
|
||||
.then(function(html) {
|
||||
fanContent.innerHTML = html;
|
||||
cards = Array.from(fanContent.querySelectorAll('.fan-card'));
|
||||
if (currentIndex >= cards.length) currentIndex = 0;
|
||||
updateFan();
|
||||
dialog.showModal();
|
||||
});
|
||||
}
|
||||
|
||||
function closeFan() {
|
||||
savePosition();
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
function navigate(delta) {
|
||||
if (!cards.length) return;
|
||||
currentIndex = (currentIndex + delta + cards.length) % cards.length;
|
||||
savePosition();
|
||||
updateFan();
|
||||
}
|
||||
|
||||
// Click on the dialog background (outside .tarot-fan-wrap) closes the modal
|
||||
dialog.addEventListener('click', function(e) {
|
||||
if (!e.target.closest('.tarot-fan-wrap')) closeFan();
|
||||
});
|
||||
|
||||
// Arrow key navigation
|
||||
dialog.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'ArrowRight') navigate(1);
|
||||
if (e.key === 'ArrowLeft') navigate(-1);
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', function() { navigate(-1); });
|
||||
nextBtn.addEventListener('click', function() { navigate(1); });
|
||||
|
||||
document.querySelectorAll('.gk-deck-card[data-deck-id]').forEach(function(card) {
|
||||
card.addEventListener('click', function() { openFan(card.dataset.deckId); });
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initGameKitPage);
|
||||
@@ -118,3 +118,21 @@ class EquipTrinketViewTest(TestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "apps/gameboard/_partials/_equip_trinket_btn.html")
|
||||
|
||||
|
||||
class TarotFanViewTest(TestCase):
|
||||
def setUp(self):
|
||||
from apps.epic.models import DeckVariant
|
||||
self.earthman = DeckVariant.objects.get(slug="earthman")
|
||||
self.fiorentine = DeckVariant.objects.get(slug="fiorentine-minchiate")
|
||||
self.user = User.objects.create(email="fan@test.io")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_returns_fan_partial_for_unlocked_deck(self):
|
||||
response = self.client.get(reverse("tarot_fan", kwargs={"deck_id": self.earthman.pk}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, "apps/gameboard/_partials/_tarot_fan.html")
|
||||
|
||||
def test_returns_403_for_locked_deck(self):
|
||||
response = self.client.get(reverse("tarot_fan", kwargs={"deck_id": self.fiorentine.pk}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@@ -8,5 +8,7 @@ urlpatterns = [
|
||||
path('toggle-applets', views.toggle_game_applets, name='toggle_game_applets'),
|
||||
path('equip-trinket/<int:token_id>/', views.equip_trinket, name='equip_trinket'),
|
||||
path('equip-deck/<int:deck_id>/', views.equip_deck, name='equip_deck'),
|
||||
path('game-kit/', views.game_kit, name='game_kit'),
|
||||
path('game-kit/deck/<int:deck_id>/', views.tarot_fan, name='tarot_fan'),
|
||||
]
|
||||
|
||||
|
||||
@@ -100,3 +100,36 @@ def equip_deck(request, deck_id):
|
||||
request.user.save(update_fields=["equipped_deck"])
|
||||
return HttpResponse(status=204)
|
||||
return HttpResponse(status=405)
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def game_kit(request):
|
||||
coin = request.user.tokens.filter(token_type=Token.COIN).first()
|
||||
pass_token = request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None
|
||||
carte = request.user.tokens.filter(token_type=Token.CARTE).first()
|
||||
free_tokens = list(request.user.tokens.filter(
|
||||
token_type=Token.FREE, expires_at__gt=timezone.now()
|
||||
).order_by("expires_at"))
|
||||
tithe_tokens = list(request.user.tokens.filter(token_type=Token.TITHE))
|
||||
return render(request, "apps/gameboard/game_kit.html", {
|
||||
"coin": coin,
|
||||
"pass_token": pass_token,
|
||||
"carte": carte,
|
||||
"free_tokens": free_tokens,
|
||||
"tithe_tokens": tithe_tokens,
|
||||
"unlocked_decks": list(request.user.unlocked_decks.all()),
|
||||
"page_class": "page-gameboard",
|
||||
})
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def tarot_fan(request, deck_id):
|
||||
from apps.epic.models import TarotCard
|
||||
deck = get_object_or_404(DeckVariant, pk=deck_id)
|
||||
if not request.user.unlocked_decks.filter(pk=deck_id).exists():
|
||||
return HttpResponse(status=403)
|
||||
cards = list(TarotCard.objects.filter(deck_variant=deck).order_by("arcana", "number"))
|
||||
return render(request, "apps/gameboard/_partials/_tarot_fan.html", {
|
||||
"deck": deck,
|
||||
"cards": cards,
|
||||
})
|
||||
|
||||
@@ -342,3 +342,163 @@ class GameKitDeckSelectionTest(FunctionalTest):
|
||||
self.browser.find_element(By.ID, "id_kit_earthman_deck")
|
||||
fiorentine_cards = self.browser.find_elements(By.ID, "id_kit_fiorentine_deck")
|
||||
self.assertEqual(len(fiorentine_cards), 0)
|
||||
|
||||
|
||||
class GameKitPageTest(FunctionalTest):
|
||||
"""
|
||||
User navigates from gameboard to the dedicated game-kit page.
|
||||
The page shows four rows: trinkets, tokens, card decks, dice placeholder.
|
||||
Clicking a deck card opens a tarot fan modal with coverflow navigation.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
from apps.epic.models import TarotCard
|
||||
for slug, name, cols, rows in [
|
||||
("new-game", "New Game", 6, 3),
|
||||
("my-games", "My Games", 6, 3),
|
||||
("game-kit", "Game Kit", 6, 3),
|
||||
]:
|
||||
Applet.objects.get_or_create(
|
||||
slug=slug,
|
||||
defaults={"name": name, "grid_cols": cols, "grid_rows": rows, "context": "gameboard"},
|
||||
)
|
||||
self.earthman, _ = DeckVariant.objects.get_or_create(
|
||||
slug="earthman",
|
||||
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
|
||||
)
|
||||
# Seed 10 cards — enough to demonstrate full 7-card coverflow
|
||||
for i in range(10):
|
||||
TarotCard.objects.get_or_create(
|
||||
deck_variant=self.earthman,
|
||||
slug=f"gkp-card-{i}",
|
||||
defaults={"name": f"Card {i}", "arcana": "MAJOR", "number": i},
|
||||
)
|
||||
# Create user after decks so signal sets equipped_deck + unlocked_decks
|
||||
self.gamer = User.objects.create(email="gamer@kit.io")
|
||||
self.gamer.refresh_from_db()
|
||||
self.create_pre_authenticated_session("gamer@kit.io")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 7 — gameboard Game Kit heading links to dedicated page #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_gameboard_game_kit_heading_links_to_game_kit_page(self):
|
||||
self.browser.get(self.live_server_url + "/gameboard/")
|
||||
link = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_applet_game_kit h2 a")
|
||||
)
|
||||
link.click()
|
||||
self.wait_for(lambda: self.assertIn("/gameboard/game-kit/", self.browser.current_url))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 8 — game-kit page shows four rows #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_game_kit_page_shows_four_rows(self):
|
||||
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_gk_trinkets"))
|
||||
self.browser.find_element(By.ID, "id_gk_tokens")
|
||||
self.browser.find_element(By.ID, "id_gk_decks")
|
||||
self.browser.find_element(By.ID, "id_gk_dice")
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 9 — clicking a deck card opens the tarot fan modal #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_clicking_deck_opens_tarot_fan_modal(self):
|
||||
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
||||
).click()
|
||||
dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
|
||||
self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 10 — fan shows active center card plus receding cards #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_fan_shows_active_card_and_receding_cards(self):
|
||||
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
||||
).click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active"))
|
||||
visible = self.browser.find_elements(
|
||||
By.CSS_SELECTOR, "#id_fan_content .fan-card:not([style*='display: none'])"
|
||||
)
|
||||
self.assertGreater(len(visible), 1)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 11 — next button advances the active card #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_fan_next_button_advances_card(self):
|
||||
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
||||
).click()
|
||||
first_index = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active")
|
||||
).get_attribute("data-index")
|
||||
self.browser.find_element(By.ID, "id_fan_next").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertNotEqual(
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active").get_attribute("data-index"),
|
||||
first_index,
|
||||
)
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 12 — clicking outside the modal closes it #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_clicking_outside_fan_closes_modal(self):
|
||||
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
||||
).click()
|
||||
dialog = self.browser.find_element(By.ID, "id_tarot_fan_dialog")
|
||||
self.wait_for(lambda: self.assertTrue(dialog.is_displayed()))
|
||||
# Dispatch a click directly on the dialog element (simulates clicking the dark backdrop)
|
||||
self.browser.execute_script(
|
||||
"document.getElementById('id_tarot_fan_dialog')"
|
||||
".dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}))"
|
||||
)
|
||||
self.wait_for(lambda: self.assertFalse(dialog.is_displayed()))
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Test 13 — reopening the modal remembers scroll position #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_fan_remembers_position_on_reopen(self):
|
||||
self.browser.get(self.live_server_url + "/gameboard/game-kit/")
|
||||
deck_card = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, "#id_gk_decks .gk-deck-card")
|
||||
)
|
||||
deck_card.click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active"))
|
||||
# Advance 3 cards
|
||||
for _ in range(3):
|
||||
self.browser.find_element(By.ID, "id_fan_next").click()
|
||||
saved_index = self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active").get_attribute("data-index")
|
||||
)
|
||||
# Close
|
||||
self.browser.execute_script(
|
||||
"document.getElementById('id_tarot_fan_dialog')"
|
||||
".dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}))"
|
||||
)
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_tarot_fan_dialog").is_displayed()
|
||||
)
|
||||
)
|
||||
# Reopen and verify position restored
|
||||
deck_card.click()
|
||||
self.wait_for(
|
||||
lambda: self.assertEqual(
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".fan-card--active").get_attribute("data-index"),
|
||||
saved_index,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -219,3 +219,4 @@
|
||||
#id_game_applets_container { @extend %applets-grid; }
|
||||
#id_wallet_applets_container { @extend %applets-grid; }
|
||||
#id_billboard_applets_container { @extend %applets-grid; }
|
||||
#id_game_kit_applets_container { @extend %applets-grid; }
|
||||
|
||||
@@ -115,3 +115,149 @@
|
||||
font-size: 0.7rem;
|
||||
color: rgba(var(--secUser), 0.4);
|
||||
}
|
||||
|
||||
// ── Game Kit page ────────────────────────────────────────────────────────────
|
||||
|
||||
#id_game_kit_applets_container section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h2 { flex-shrink: 0; }
|
||||
.gk-items { flex: 1; overflow-y: auto; }
|
||||
}
|
||||
|
||||
.gk-items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gk-deck-card,
|
||||
.gk-trinket-card,
|
||||
.gk-token-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 0.1rem solid rgba(var(--secUser), 0.3);
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
min-width: 6rem;
|
||||
text-align: center;
|
||||
transition: border-color 0.15s;
|
||||
|
||||
span { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
small { font-size: 0.6rem; opacity: 0.5; }
|
||||
|
||||
&:hover { border-color: rgba(var(--secUser), 0.8); }
|
||||
}
|
||||
|
||||
.gk-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 0.1rem dashed rgba(var(--secUser), 0.2);
|
||||
font-size: 1.5rem;
|
||||
min-width: 6rem;
|
||||
text-align: center;
|
||||
opacity: 0.4;
|
||||
|
||||
span { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; }
|
||||
}
|
||||
|
||||
.gk-empty {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
// ── Tarot fan modal ──────────────────────────────────────────────────────────
|
||||
|
||||
#id_tarot_fan_dialog {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
overflow: hidden;
|
||||
|
||||
&::backdrop { display: none; } // Dialog IS the backdrop
|
||||
}
|
||||
|
||||
.tarot-fan-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
perspective: 900px;
|
||||
}
|
||||
|
||||
.tarot-fan {
|
||||
position: relative;
|
||||
width: 220px;
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
.fan-card {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 220px;
|
||||
height: 340px;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(var(--priUser), 1);
|
||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
&--active {
|
||||
border-color: rgba(var(--secUser), 1);
|
||||
box-shadow: 0 0 2rem rgba(var(--secUser), 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.fan-card-face {
|
||||
padding: 1.25rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.fan-card-number { font-size: 0.65rem; opacity: 0.5; }
|
||||
.fan-card-name { font-size: 0.95rem; font-weight: bold; margin: 0; }
|
||||
.fan-card-arcana { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.1em; opacity: 0.6; }
|
||||
.fan-card-correspondence { font-size: 0.6rem; opacity: 0.45; font-style: italic; }
|
||||
}
|
||||
|
||||
.fan-nav {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(var(--secUser), 0.6);
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
transition: color 0.15s;
|
||||
pointer-events: auto;
|
||||
|
||||
&:hover { color: rgba(var(--secUser), 1); }
|
||||
&--prev { left: 1rem; }
|
||||
&--next { right: 1rem; }
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
id="id_applet_game_kit"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2>Game Kit</h2>
|
||||
<h2><a href="{% url 'game_kit' %}">Game Kit</a></h2>
|
||||
<div id="id_game_kit" data-equipped-id="{{ equipped_trinket_id }}" data-equipped-deck-id="{{ equipped_deck_id }}">
|
||||
{% if pass_token %}
|
||||
<div id="id_kit_pass" class="token" data-token-id="{{ pass_token.pk }}">
|
||||
|
||||
15
src/templates/apps/gameboard/_partials/_tarot_fan.html
Normal file
15
src/templates/apps/gameboard/_partials/_tarot_fan.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% for card in cards %}
|
||||
<div class="fan-card" data-index="{{ forloop.counter0 }}">
|
||||
<div class="fan-card-face">
|
||||
<p class="fan-card-number">{{ card.number }}</p>
|
||||
<h3 class="fan-card-name">{{ card.name }}</h3>
|
||||
<p class="fan-card-arcana">{{ card.get_arcana_display }}</p>
|
||||
{% if card.correspondence %}
|
||||
<p class="fan-card-correspondence">{{ card.correspondence }}</p>
|
||||
{% endif %}
|
||||
{% if card.suit %}
|
||||
<p class="fan-card-suit">{{ card.suit }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
98
src/templates/apps/gameboard/game_kit.html
Normal file
98
src/templates/apps/gameboard/game_kit.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{% extends "core/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title_text %}Game Kit{% endblock title_text %}
|
||||
{% block header_text %}<span>Game</span>Kit{% endblock header_text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="gameboard-page">
|
||||
<div id="id_game_kit_applets_container">
|
||||
|
||||
<section id="id_gk_trinkets" style="--applet-cols: 6; --applet-rows: 3;">
|
||||
<h2>Trinkets</h2>
|
||||
<div class="gk-items">
|
||||
{% if pass_token %}
|
||||
<div class="gk-trinket-card" data-token-id="{{ pass_token.pk }}">
|
||||
<i class="fa-solid fa-clipboard"></i>
|
||||
<span>{{ pass_token.tooltip_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if carte %}
|
||||
<div class="gk-trinket-card" data-token-id="{{ carte.pk }}">
|
||||
<i class="fa-solid fa-money-check"></i>
|
||||
<span>{{ carte.tooltip_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if coin %}
|
||||
<div class="gk-trinket-card" data-token-id="{{ coin.pk }}">
|
||||
<i class="fa-solid fa-medal"></i>
|
||||
<span>{{ coin.tooltip_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not pass_token and not carte and not coin %}
|
||||
<p class="gk-empty"><em>No trinkets yet.</em></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="id_gk_tokens" style="--applet-cols: 6; --applet-rows: 3;">
|
||||
<h2>Tokens</h2>
|
||||
<div class="gk-items">
|
||||
{% for token in free_tokens %}
|
||||
<div class="gk-token-card" data-token-id="{{ token.pk }}">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
<span>{{ token.tooltip_name }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for token in tithe_tokens %}
|
||||
<div class="gk-token-card" data-token-id="{{ token.pk }}">
|
||||
<i class="fa-solid fa-hand-holding-dollar"></i>
|
||||
<span>{{ token.tooltip_name }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not free_tokens and not tithe_tokens %}
|
||||
<p class="gk-empty"><em>No tokens yet.</em></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="id_gk_decks" style="--applet-cols: 6; --applet-rows: 3;">
|
||||
<h2>Card Decks</h2>
|
||||
<div class="gk-items">
|
||||
{% for deck in unlocked_decks %}
|
||||
<div class="gk-deck-card" data-deck-id="{{ deck.pk }}">
|
||||
<i class="fa-regular fa-id-badge"></i>
|
||||
<span>{{ deck.name }}</span>
|
||||
<small>{{ deck.card_count }} cards</small>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="gk-empty"><em>No decks unlocked.</em></p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="id_gk_dice" style="--applet-cols: 6; --applet-rows: 3;">
|
||||
<h2>Dice Sets</h2>
|
||||
<div class="gk-items">
|
||||
<div class="gk-placeholder">
|
||||
<i class="fa-solid fa-dice"></i>
|
||||
<span>Coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="id_tarot_fan_dialog">
|
||||
<div class="tarot-fan-wrap">
|
||||
<button id="id_fan_prev" class="fan-nav fan-nav--prev" aria-label="Previous card">‹</button>
|
||||
<div id="id_fan_content" class="tarot-fan"></div>
|
||||
<button id="id_fan_next" class="fan-nav fan-nav--next" aria-label="Next card">›</button>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endblock content %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{% static 'apps/gameboard/game-kit.js' %}"></script>
|
||||
{% endblock scripts %}
|
||||
Reference in New Issue
Block a user