My Sea iter 6c: bud-btn invite stub + #id_my_sea_menu gear (NVM-only, %applet-menu-styled, on both /gameboard/my-sea/ and the gatekeeper) + PAID DRAW now deletes the row and redirects to ?phase=picker so the user drops straight into picking cards instead of looping back to GATE VIEW — Sprint 5 iter 6c of My Sea roadmap — TDD
Bundled fix for the PAID-DRAW-loops-to-GATE-VIEW bug surfaced 2026-05-20 in live testing: previously the view reset `created_at = now()` + cleared the hand, but the row's continued existence meant `quota_spent=True` on the next render → landing rendered GATE VIEW → user clicked it → back to gatekeeper → loop. Now PAID DRAW does `active_draw.delete()` after debiting the token + then redirects to `/gameboard/my-sea/?phase=picker`. The my_sea view honors `?phase=picker` (only when no active_draw exists — can't bypass post-DEL GATE VIEW) by forcing `show_picker=True` so the user lands in the picker ready to draw. First card draw creates a fresh row w. fresh `created_at`, starting the new 24h quota cycle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1563,32 +1563,22 @@ class MySeaPaidDrawViewTest(TestCase):
|
|||||||
self.client.post(self.url)
|
self.client.post(self.url)
|
||||||
self.assertFalse(Token.objects.filter(pk=self.free_tok.pk).exists())
|
self.assertFalse(Token.objects.filter(pk=self.free_tok.pk).exists())
|
||||||
|
|
||||||
def test_paid_draw_clears_deposit_fields_and_resets_created_at(self):
|
def test_paid_draw_deletes_active_draw_row(self):
|
||||||
old_created = self.draw.created_at
|
# User-spec 2026-05-20: PAID DRAW commits the token + drops the row
|
||||||
# Push old created_at back so reset is observable.
|
# entirely so the user returns to a fresh "able-to-draw-now" state
|
||||||
from datetime import timedelta
|
# (instead of the buggy "row preserved → GATE VIEW loop" semantics).
|
||||||
from apps.gameboard.models import MySeaDraw
|
from apps.gameboard.models import MySeaDraw
|
||||||
MySeaDraw.objects.filter(pk=self.draw.pk).update(
|
|
||||||
created_at=old_created - timedelta(hours=12),
|
|
||||||
)
|
|
||||||
self.client.post(self.url)
|
self.client.post(self.url)
|
||||||
self.draw.refresh_from_db()
|
self.assertFalse(MySeaDraw.objects.filter(pk=self.draw.pk).exists())
|
||||||
self.assertIsNone(self.draw.deposit_token_id)
|
|
||||||
self.assertIsNone(self.draw.deposit_reserved_at)
|
|
||||||
self.assertGreater(self.draw.created_at, old_created - timedelta(hours=12))
|
|
||||||
|
|
||||||
def test_paid_draw_resets_hand_to_empty(self):
|
def test_paid_draw_redirects_to_my_sea_with_phase_picker(self):
|
||||||
# Even if hand is non-empty, PAID DRAW wipes it (fresh draw cycle).
|
# User-spec 2026-05-20: drop the user directly into the picker
|
||||||
from apps.epic.models import TarotCard
|
# after PAID DRAW (no intermediate FREE-DRAW click). Encoded via
|
||||||
cards = list(TarotCard.objects.exclude(id=self.target.id)[:2])
|
# `?phase=picker` query param so the my_sea view can short-
|
||||||
self.draw.hand = [
|
# circuit `show_picker` even when active_draw is now None.
|
||||||
{"position": "lay", "card_id": cards[0].id, "reversed": False, "polarity": "gravity"},
|
response = self.client.post(self.url)
|
||||||
{"position": "cover", "card_id": cards[1].id, "reversed": True, "polarity": "levity"},
|
self.assertEqual(response.status_code, 302)
|
||||||
]
|
self.assertIn("phase=picker", response["Location"])
|
||||||
self.draw.save(update_fields=["hand"])
|
|
||||||
self.client.post(self.url)
|
|
||||||
self.draw.refresh_from_db()
|
|
||||||
self.assertEqual(self.draw.hand, [])
|
|
||||||
|
|
||||||
def test_paid_draw_with_coin_sets_24h_cooldown_and_unequips(self):
|
def test_paid_draw_with_coin_sets_24h_cooldown_and_unequips(self):
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -1621,6 +1611,45 @@ class MySeaPaidDrawViewTest(TestCase):
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
|
||||||
|
class MySeaPhasePickerQueryParamTest(TestCase):
|
||||||
|
"""Sprint 6 iter 6c — `?phase=picker` query param forces picker phase
|
||||||
|
when no active_draw row exists (the just-after-PAID-DRAW state).
|
||||||
|
Without the param, no-active-draw users default to the FREE DRAW
|
||||||
|
landing. With it, they drop straight into the picker so they can
|
||||||
|
start drawing immediately (the token they just spent earns this)."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from apps.epic.models import personal_sig_cards
|
||||||
|
self.user = User.objects.create(email="phase@test.io")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
self.target = personal_sig_cards(self.user)[0]
|
||||||
|
self.user.significator = self.target
|
||||||
|
self.user.save(update_fields=["significator"])
|
||||||
|
|
||||||
|
def test_no_param_lands_on_free_draw(self):
|
||||||
|
response = self.client.get(reverse("my_sea"))
|
||||||
|
self.assertContains(response, 'data-phase="landing"')
|
||||||
|
self.assertContains(response, 'id="id_draw_sea_btn"')
|
||||||
|
|
||||||
|
def test_phase_picker_param_forces_picker(self):
|
||||||
|
response = self.client.get(reverse("my_sea") + "?phase=picker")
|
||||||
|
self.assertContains(response, 'data-phase="picker"')
|
||||||
|
# Picker IS rendered (no inline style="display:none" on it).
|
||||||
|
self.assertNotContains(response, 'id="id_sea_overlay"' + ' style="display:none"')
|
||||||
|
|
||||||
|
def test_phase_picker_param_ignored_when_active_draw_with_empty_hand(self):
|
||||||
|
# Post-DEL state: active row w. empty hand → quota's spent, the
|
||||||
|
# query param shouldn't bypass GATE VIEW. Landing branch wins.
|
||||||
|
from apps.gameboard.models import MySeaDraw
|
||||||
|
MySeaDraw.objects.create(
|
||||||
|
user=self.user, spread="situation-action-outcome",
|
||||||
|
significator_id=self.target.id, hand=[],
|
||||||
|
)
|
||||||
|
response = self.client.get(reverse("my_sea") + "?phase=picker")
|
||||||
|
self.assertContains(response, 'data-phase="landing"')
|
||||||
|
self.assertContains(response, 'id="id_my_sea_gate_view_btn"')
|
||||||
|
|
||||||
|
|
||||||
class SelectMySeaTokenTest(TestCase):
|
class SelectMySeaTokenTest(TestCase):
|
||||||
"""Sprint 6 iter 6a — `_select_my_sea_token` priority chain w. CARTE
|
"""Sprint 6 iter 6a — `_select_my_sea_token` priority chain w. CARTE
|
||||||
excluded + COIN cooldown-respecting."""
|
excluded + COIN cooldown-respecting."""
|
||||||
|
|||||||
@@ -20,5 +20,6 @@ urlpatterns = [
|
|||||||
path('my-sea/insert', views.my_sea_insert_token, name='my_sea_insert_token'),
|
path('my-sea/insert', views.my_sea_insert_token, name='my_sea_insert_token'),
|
||||||
path('my-sea/refund', views.my_sea_refund_token, name='my_sea_refund_token'),
|
path('my-sea/refund', views.my_sea_refund_token, name='my_sea_refund_token'),
|
||||||
path('my-sea/paid-draw', views.my_sea_paid_draw, name='my_sea_paid_draw'),
|
path('my-sea/paid-draw', views.my_sea_paid_draw, name='my_sea_paid_draw'),
|
||||||
|
path('my-sea/invite', views.my_sea_invite, name='my_sea_invite'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,18 @@ def my_sea(request):
|
|||||||
# (the daily quota's spent already; landing's primary nav routes to
|
# (the daily quota's spent already; landing's primary nav routes to
|
||||||
# the upcoming gatekeeper). New users + post-24h users land on the
|
# the upcoming gatekeeper). New users + post-24h users land on the
|
||||||
# standard FREE DRAW landing.
|
# standard FREE DRAW landing.
|
||||||
show_picker = active_draw is not None and not hand_empty
|
#
|
||||||
|
# `?phase=picker` query param (set by PAID DRAW's redirect) forces
|
||||||
|
# the picker even when active_draw is None — the user just paid a
|
||||||
|
# token, so drop them straight into the picker rather than making
|
||||||
|
# them click FREE DRAW first. Only honored when active_draw is None
|
||||||
|
# (post-PAID-DRAW state); existing rows route through the normal
|
||||||
|
# logic above so the param can't accidentally bypass a GATE VIEW
|
||||||
|
# or empty-hand state.
|
||||||
|
phase_param = request.GET.get("phase") == "picker"
|
||||||
|
show_picker = (active_draw is not None and not hand_empty) or (
|
||||||
|
active_draw is None and phase_param
|
||||||
|
)
|
||||||
quota_spent = active_draw is not None # any active row = quota committed
|
quota_spent = active_draw is not None # any active row = quota committed
|
||||||
# Sprint 6 iter 6b — landing center-btn 3-way + seat-1 persistence.
|
# Sprint 6 iter 6b — landing center-btn 3-way + seat-1 persistence.
|
||||||
# `deposit_reserved` toggles the landing primary from GATE VIEW to
|
# `deposit_reserved` toggles the landing primary from GATE VIEW to
|
||||||
@@ -453,11 +464,23 @@ def my_sea_refund_token(request):
|
|||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
@require_POST
|
@require_POST
|
||||||
def my_sea_paid_draw(request):
|
def my_sea_paid_draw(request):
|
||||||
"""Commit the deposited token + reset the row for a fresh quota
|
"""Commit the deposited token + drop the active_draw row so the
|
||||||
cycle. The token is debited via `debit_my_sea_token` (FREE/TITHE
|
user returns to a fresh "able-to-draw-now" state. Without the row,
|
||||||
consumed; COIN 24h cooldown + unequipped; PASS no-op). Hand wiped,
|
`quota_spent` resolves to False on the next my-sea render → the
|
||||||
`created_at` reset to now, deposit fields cleared. User redirects
|
user can draw cards immediately (the token they just spent earns
|
||||||
back to /gameboard/my-sea/ ready to draw a fresh hand."""
|
them this 24h cycle's worth of draws).
|
||||||
|
|
||||||
|
The token is debited via `debit_my_sea_token` (FREE/TITHE consumed;
|
||||||
|
COIN 24h cooldown + unequipped; PASS no-op). The row is then
|
||||||
|
deleted (rather than just reset) — user-spec 2026-05-20: keeping
|
||||||
|
the row but resetting created_at left `quota_spent=True` on the
|
||||||
|
next view, looping the user back to GATE VIEW. Delete sidesteps
|
||||||
|
that entirely.
|
||||||
|
|
||||||
|
Redirects to /gameboard/my-sea/?phase=picker so the user lands
|
||||||
|
directly in the picker (skipping the FREE DRAW landing click).
|
||||||
|
"""
|
||||||
|
from django.urls import reverse
|
||||||
from apps.lyric.models import Token
|
from apps.lyric.models import Token
|
||||||
active_draw = active_draw_for(request.user)
|
active_draw = active_draw_for(request.user)
|
||||||
if active_draw is None or active_draw.deposit_token_id is None:
|
if active_draw is None or active_draw.deposit_token_id is None:
|
||||||
@@ -473,14 +496,28 @@ def my_sea_paid_draw(request):
|
|||||||
active_draw.save(update_fields=["deposit_token_id", "deposit_reserved_at"])
|
active_draw.save(update_fields=["deposit_token_id", "deposit_reserved_at"])
|
||||||
return redirect("my_sea")
|
return redirect("my_sea")
|
||||||
debit_my_sea_token(request.user, token)
|
debit_my_sea_token(request.user, token)
|
||||||
active_draw.hand = []
|
active_draw.delete()
|
||||||
active_draw.created_at = timezone.now()
|
return redirect(reverse("my_sea") + "?phase=picker")
|
||||||
active_draw.deposit_token_id = None
|
|
||||||
active_draw.deposit_reserved_at = None
|
|
||||||
active_draw.save(update_fields=[
|
@login_required(login_url="/")
|
||||||
"hand", "created_at", "deposit_token_id", "deposit_reserved_at",
|
@require_POST
|
||||||
])
|
def my_sea_invite(request):
|
||||||
return redirect("my_sea")
|
"""Sprint 6 iter 6c — bud-btn invite stub on my-sea gatekeeper.
|
||||||
|
Async multi-user invite is deferred to a later sprint; this endpoint
|
||||||
|
just returns a Brief banner announcing "coming soon" so the bud-btn
|
||||||
|
panel has a non-broken success path."""
|
||||||
|
from django.urls import reverse
|
||||||
|
return JsonResponse({
|
||||||
|
"brief": {
|
||||||
|
"title": "Multiplayer my-sea",
|
||||||
|
"line_text": "Look!—multiplayer my-sea is coming soon. Stay tuned.",
|
||||||
|
"post_url": reverse("gameboard"),
|
||||||
|
"created_at": "",
|
||||||
|
"kind": "NUDGE",
|
||||||
|
},
|
||||||
|
"recipient_display": (request.POST.get("recipient") or "").strip(),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def _my_sea_deck_data(user, exclude_id=None):
|
def _my_sea_deck_data(user, exclude_id=None):
|
||||||
|
|||||||
@@ -1561,3 +1561,98 @@ class MySeaBudBtnStubTest(FunctionalTest):
|
|||||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner")
|
lambda: self.browser.find_element(By.CSS_SELECTOR, ".note-banner")
|
||||||
)
|
)
|
||||||
self.assertIn("coming soon", brief.text.lower())
|
self.assertIn("coming soon", brief.text.lower())
|
||||||
|
|
||||||
|
|
||||||
|
class MySeaGearBtnTest(FunctionalTest):
|
||||||
|
"""Sprint 6 iter 6c — `.gear-btn` on every my-sea page state
|
||||||
|
(landing / picker / gatekeeper). Opens a NVM-only menu (DEL/BYE
|
||||||
|
deliberately omitted — gear is for "back out without committing");
|
||||||
|
NVM nav-backs to /gameboard/ mirroring the room's gear-menu
|
||||||
|
convention. Same partial included on both /gameboard/my-sea/ and
|
||||||
|
/gameboard/my-sea/gate/ so visual + behavior is identical."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
_seed_earthman_sig_pile()
|
||||||
|
_seed_gameboard_applets()
|
||||||
|
self.email = "gear@test.io"
|
||||||
|
self.gamer = User.objects.create(email=self.email)
|
||||||
|
_assign_sig(self.gamer)
|
||||||
|
# Seed a quota row so the gatekeeper has an active_draw context.
|
||||||
|
from apps.gameboard.models import MySeaDraw
|
||||||
|
MySeaDraw.objects.create(
|
||||||
|
user=self.gamer, spread="situation-action-outcome",
|
||||||
|
significator_id=self.gamer.significator_id, hand=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_gear_btn_renders_on_gatekeeper(self):
|
||||||
|
self.create_pre_authenticated_session(self.email)
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/")
|
||||||
|
gear = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".my-sea-page .gear-btn"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Menu wired via data-menu-target.
|
||||||
|
self.assertEqual(
|
||||||
|
gear.get_attribute("data-menu-target"),
|
||||||
|
"id_my_sea_menu",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_gear_btn_renders_on_landing_without_active_draw(self):
|
||||||
|
"""User direction 2026-05-20 — gear stays on /gameboard/my-sea/
|
||||||
|
even when no gatekeeper / no active_draw row exists. Fresh user
|
||||||
|
with a sig + no draw lands here + still sees the gear."""
|
||||||
|
from apps.gameboard.models import MySeaDraw
|
||||||
|
MySeaDraw.objects.filter(user=self.gamer).delete()
|
||||||
|
self.create_pre_authenticated_session(self.email)
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".my-sea-page .gear-btn"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_gear_btn_opens_menu_with_nvm_only(self):
|
||||||
|
self.create_pre_authenticated_session(self.email)
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/")
|
||||||
|
gear = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".my-sea-page .gear-btn"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
gear.click()
|
||||||
|
menu = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.ID, "id_my_sea_menu")
|
||||||
|
)
|
||||||
|
self.assertEqual(menu.value_of_css_property("display"), "block")
|
||||||
|
# NVM link present, DEL + BYE deliberately absent.
|
||||||
|
nvm = menu.find_element(By.CSS_SELECTOR, ".btn-cancel")
|
||||||
|
self.assertIn("/gameboard/", nvm.get_attribute("href"))
|
||||||
|
self.assertEqual(
|
||||||
|
len(menu.find_elements(By.CSS_SELECTOR, ".btn-danger")), 0,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
len(menu.find_elements(By.CSS_SELECTOR, ".btn-abandon")), 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nvm_navigates_back_to_gameboard(self):
|
||||||
|
self.create_pre_authenticated_session(self.email)
|
||||||
|
self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/")
|
||||||
|
gear = self.wait_for(
|
||||||
|
lambda: self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, ".my-sea-page .gear-btn"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
gear.click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.browser.find_element(By.ID, "id_my_sea_menu")
|
||||||
|
)
|
||||||
|
self.browser.find_element(
|
||||||
|
By.CSS_SELECTOR, "#id_my_sea_menu .btn-cancel"
|
||||||
|
).click()
|
||||||
|
self.wait_for(
|
||||||
|
lambda: self.assertRegex(
|
||||||
|
self.browser.current_url, r"/gameboard/$"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
#id_post_menu { @extend %applet-menu; }
|
#id_post_menu { @extend %applet-menu; }
|
||||||
#id_billboard_applet_menu { @extend %applet-menu; }
|
#id_billboard_applet_menu { @extend %applet-menu; }
|
||||||
#id_billscroll_menu { @extend %applet-menu; }
|
#id_billscroll_menu { @extend %applet-menu; }
|
||||||
|
#id_my_sea_menu { @extend %applet-menu; }
|
||||||
|
|
||||||
// Page-level gear buttons — fixed to viewport bottom-right
|
// Page-level gear buttons — fixed to viewport bottom-right
|
||||||
.gameboard-page,
|
.gameboard-page,
|
||||||
@@ -93,7 +94,8 @@
|
|||||||
.room-page,
|
.room-page,
|
||||||
.post-page,
|
.post-page,
|
||||||
.billboard-page,
|
.billboard-page,
|
||||||
.billscroll-page {
|
.billscroll-page,
|
||||||
|
.my-sea-page {
|
||||||
> .gear-btn {
|
> .gear-btn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 4.2rem;
|
bottom: 4.2rem;
|
||||||
@@ -108,7 +110,8 @@
|
|||||||
#id_wallet_applet_menu,
|
#id_wallet_applet_menu,
|
||||||
#id_post_menu,
|
#id_post_menu,
|
||||||
#id_billboard_applet_menu,
|
#id_billboard_applet_menu,
|
||||||
#id_billscroll_menu {
|
#id_billscroll_menu,
|
||||||
|
#id_my_sea_menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 6.6rem;
|
bottom: 6.6rem;
|
||||||
right: 1rem;
|
right: 1rem;
|
||||||
@@ -126,7 +129,8 @@
|
|||||||
.room-page,
|
.room-page,
|
||||||
.post-page,
|
.post-page,
|
||||||
.billboard-page,
|
.billboard-page,
|
||||||
.billscroll-page {
|
.billscroll-page,
|
||||||
|
.my-sea-page {
|
||||||
> .gear-btn {
|
> .gear-btn {
|
||||||
right: calc((var(--sidebar-w) - 3rem) / 2);
|
right: calc((var(--sidebar-w) - 3rem) / 2);
|
||||||
bottom: 3.95rem;
|
bottom: 3.95rem;
|
||||||
@@ -141,7 +145,8 @@
|
|||||||
#id_room_menu,
|
#id_room_menu,
|
||||||
#id_post_menu,
|
#id_post_menu,
|
||||||
#id_billboard_applet_menu,
|
#id_billboard_applet_menu,
|
||||||
#id_billscroll_menu {
|
#id_billscroll_menu,
|
||||||
|
#id_my_sea_menu {
|
||||||
right: calc((var(--sidebar-w) - 3rem) / 2);
|
right: calc((var(--sidebar-w) - 3rem) / 2);
|
||||||
bottom: 6.6rem;
|
bottom: 6.6rem;
|
||||||
top: auto;
|
top: auto;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{% load static %}
|
||||||
|
{# Sprint 6 iter 6c — bud-btn invite panel on the my-sea gatekeeper. #}
|
||||||
|
{# Mirrors `_bud_invite_panel.html` (room) but POSTs to a stub view — #}
|
||||||
|
{# real async multi-user invite is deferred to a future sprint, so OK #}
|
||||||
|
{# returns a 'Multiplayer my-sea coming soon' Brief banner. #}
|
||||||
|
|
||||||
|
<button id="id_bud_btn" type="button" aria-label="Invite a friend">
|
||||||
|
<i class="fa-solid fa-handshake"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="id_bud_panel">
|
||||||
|
<input id="id_recipient"
|
||||||
|
name="recipient"
|
||||||
|
type="text"
|
||||||
|
placeholder="friend@example.com or username"
|
||||||
|
autocomplete="off">
|
||||||
|
<button id="id_bud_ok" type="button" class="btn btn-confirm">OK</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="id_bud_suggestions" class="bud-suggestions" hidden></div>
|
||||||
|
|
||||||
|
<script src="{% static 'apps/billboard/bud-autocomplete.js' %}"></script>
|
||||||
|
<script src="{% static 'apps/billboard/bud-btn.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
bindBudBtn({
|
||||||
|
submitUrl: '{% url "my_sea_invite" %}',
|
||||||
|
autocompleteUrl: '{% url "billboard:search_buds" %}',
|
||||||
|
onSuccess: function (data) {
|
||||||
|
if (window.Brief && data.brief) Brief.showBanner(data.brief);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
9
src/templates/apps/gameboard/_partials/_my_sea_gear.html
Normal file
9
src/templates/apps/gameboard/_partials/_my_sea_gear.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{# Sprint 6 iter 6c — gear-btn on /gameboard/my-sea/ + the gatekeeper. #}
|
||||||
|
{# NVM-only menu (no DEL, no BYE) — gear is the "back out without #}
|
||||||
|
{# committing" affordance; NVM nav-backs to /gameboard/ mirroring the #}
|
||||||
|
{# room's gear-menu convention. Rendered unconditionally (no active- #}
|
||||||
|
{# draw guard) so fresh users + post-DEL states still see it. #}
|
||||||
|
<div id="id_my_sea_menu" style="display:none">
|
||||||
|
<a href="{% url 'gameboard' %}" class="btn btn-cancel">NVM</a>
|
||||||
|
</div>
|
||||||
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_my_sea_menu" %}
|
||||||
@@ -900,5 +900,9 @@
|
|||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{# Sprint 6 iter 6c — gear-btn lives on every my-sea page state #}
|
||||||
|
{# (sign-gate / landing / picker). NVM-only menu mirrors the #}
|
||||||
|
{# gatekeeper's gear; "back out to /gameboard/" affordance. #}
|
||||||
|
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -82,5 +82,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Sprint 6 iter 6c — bud-btn invite stub (multiplayer-coming-soon) #}
|
||||||
|
{# + gear-btn NVM-only menu. Both render outside .gate-modal so the #}
|
||||||
|
{# bud-btn lives at viewport fixed position (per `_bud.scss`) and #}
|
||||||
|
{# the gear-btn sits atop `#id_kit_btn` in the bottom-right corner. #}
|
||||||
|
{% include "apps/gameboard/_partials/_my_sea_bud_panel.html" %}
|
||||||
|
{% include "apps/gameboard/_partials/_my_sea_gear.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{# Brief module — needed by _my_sea_bud_panel's OK handler so the #}
|
||||||
|
{# 'Multiplayer my-sea coming soon' banner shows on a successful #}
|
||||||
|
{# (stub) invite. #}
|
||||||
|
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
||||||
|
{% endblock scripts %}
|
||||||
|
|||||||
Reference in New Issue
Block a user