diff --git a/src/apps/gameboard/views.py b/src/apps/gameboard/views.py index d0e40e8..07859cc 100644 --- a/src/apps/gameboard/views.py +++ b/src/apps/gameboard/views.py @@ -221,6 +221,15 @@ def my_sea(request): # standard FREE DRAW landing. show_picker = active_draw is not None and not hand_empty quota_spent = active_draw is not None # any active row = quota committed + # Sprint 6 iter 6b — landing center-btn 3-way + seat-1 persistence. + # `deposit_reserved` toggles the landing primary from GATE VIEW to + # PAID DRAW (one-click commit of the already-deposited token). + # `hand_non_empty` lifts seat 1 to `.seated` server-side so reloads + # don't lose the JS-only animation state. + deposit_reserved = ( + active_draw is not None and active_draw.deposit_token_id is not None + ) + hand_non_empty = active_draw is not None and bool(active_draw.hand) # Per-position lookup for the template — keyed by the position slug # ("lay", "cover", ...) so each `.sea-pos-` block can render @@ -265,6 +274,8 @@ def my_sea(request): "hand_complete": hand_complete, "show_picker": show_picker, "quota_spent": quota_spent, + "deposit_reserved": deposit_reserved, + "hand_non_empty": hand_non_empty, "page_class": "page-gameboard page-my-sea", }) diff --git a/src/functional_tests/test_game_my_sea.py b/src/functional_tests/test_game_my_sea.py index 53651f2..b48500d 100644 --- a/src/functional_tests/test_game_my_sea.py +++ b/src/functional_tests/test_game_my_sea.py @@ -1200,6 +1200,12 @@ class MySeaGatekeeperPageTest(FunctionalTest): self.email = "gate@test.io" self.gamer = User.objects.create(email=self.email) _assign_sig(self.gamer) + # The User post_save signal auto-creates COIN + FREE tokens + # (apps.lyric.models). Clear them so the deposit-priority test + # below picks only the FREE we seed explicitly — otherwise + # COIN wins (PASS > COIN > FREE > TITHE) and the FREE-count + # assertion fails (per IT-trap memo, 2026-05-19). + self.gamer.tokens.all().delete() # Seed a FREE token so the gatekeeper has something to deposit. from datetime import timedelta from django.utils import timezone as dj_tz @@ -1245,27 +1251,27 @@ class MySeaGatekeeperPageTest(FunctionalTest): 0, ) - def test_gatekeeper_renders_six_chair_seats_with_seat1_seated(self): - """Hex w. 6 chair seats; seat 1 is the owner's (always `.seated` - when quota is committed); seats 2-6 carry `.fa-ban` (placeholders - for the future friend-invite feature).""" + def test_gatekeeper_renders_no_hex_modal_only(self): + """Per user spec 2026-05-20, the gatekeeper is a transient in-flight + UI — modal-over-`--duoUser` bg, NO hex / chair-seats. Seats live on + the my-sea picker page itself; the gatekeeper just offers the + coin-slot + (post-deposit) PAID DRAW affordance.""" self._save_empty_hand_draw() self.create_pre_authenticated_session(self.email) self.browser.get(self.live_server_url + "/gameboard/my-sea/gate/") self.wait_for( - lambda: self._assert_seats(6) + lambda: self.browser.find_element( + By.CSS_SELECTOR, ".my-sea-gate-modal" + ) ) - seat1 = self.browser.find_element( - By.CSS_SELECTOR, ".table-seat[data-slot='1']" + self.assertEqual( + len(self.browser.find_elements(By.CSS_SELECTOR, ".table-seat")), + 0, + ) + self.assertEqual( + len(self.browser.find_elements(By.CSS_SELECTOR, ".table-hex")), + 0, ) - self.assertIn("seated", seat1.get_attribute("class")) - seat1.find_element(By.CSS_SELECTOR, ".fa-circle-check") - - def _assert_seats(self, count): - seats = self.browser.find_elements(By.CSS_SELECTOR, ".table-seat") - if len(seats) != count: - raise AssertionError(f"expected {count} seats, got {len(seats)}") - return seats def test_insert_token_reserves_deposit_and_reveals_paid_draw_btn(self): """Click INSERT TOKEN → server reserves the user's next-priority diff --git a/src/static_src/scss/_base.scss b/src/static_src/scss/_base.scss index e593031..3f8ddce 100644 --- a/src/static_src/scss/_base.scss +++ b/src/static_src/scss/_base.scss @@ -90,7 +90,8 @@ body { > form { flex-shrink: 0; order: -1; } // BYE left of spans } - > #id_cont_game { flex-shrink: 0; } + > #id_cont_game, + > #id_navbar_gate_view_btn { flex-shrink: 0; } } .navbar-text, @@ -306,7 +307,8 @@ body { padding: 0 0.25rem; margin: 0; // reset portrait margin-right: 0.5rem so container fills full sidebar width - > #id_cont_game { flex-shrink: 0; order: -1; } // cont-game above brand + > #id_cont_game, + > #id_navbar_gate_view_btn { flex-shrink: 0; order: -1; } // cont-game / GATE VIEW above brand .navbar-user { flex-direction: column; diff --git a/src/templates/apps/gameboard/my_sea.html b/src/templates/apps/gameboard/my_sea.html index 8e5cdef..e460b99 100644 --- a/src/templates/apps/gameboard/my_sea.html +++ b/src/templates/apps/gameboard/my_sea.html @@ -33,13 +33,21 @@ {# friend-invite feature per the My Sea roadmap architectural #} {# anchor "Six chairs retained even in solo". #} {# #} - {# Iter 4c — landing primary btn is: #} - {# • FREE DRAW (`#id_draw_sea_btn`) when the user has no #} - {# active quota row (fresh user OR >24h since last draw); #} - {# • GATE VIEW (`#id_my_sea_gate_view_btn`) when the user's #} - {# quota row exists but the hand is empty (post-DEL). The #} - {# daily free draw is spent; further draws require a token #} - {# deposit via the Sprint-6 gatekeeper (currently 404). #} + {# Iter 6b — landing primary-btn state machine, 3-way: #} + {# • FREE DRAW (`#id_draw_sea_btn`) when no active quota row #} + {# (fresh user OR >24h since last draw); #} + {# • PAID DRAW (`#id_my_sea_paid_draw_btn`) when the user has #} + {# deposited a token via the gatekeeper but hasn't yet #} + {# committed it; one-click POST commits + redirects to #} + {# fresh-quota /gameboard/my-sea/. #} + {# • GATE VIEW (`#id_my_sea_gate_view_btn`) when the user's #} + {# quota row exists, the hand is empty, AND no deposit yet #} + {# (post-DEL state). Routes to the Sprint-6 gatekeeper. #} + {# Seat 1 (`data-slot="1"`) carries `.seated` + `.fa-circle- #} + {# check` when the user's hand is non-empty (server-rendered #} + {# so reloads preserve the state). Otherwise `.fa-ban`. Other #} + {# 5 seats are placeholders for the future friend-invite #} + {# feature — always banned in solo my-sea. #}
@@ -47,7 +55,14 @@
- {% if quota_spent %} + {% if deposit_reserved %} +
+ {% csrf_token %} + +
+ {% elif quota_spent %}
diff --git a/src/templates/core/_partials/_navbar.html b/src/templates/core/_partials/_navbar.html index cc2d74c..0331df8 100644 --- a/src/templates/core/_partials/_navbar.html +++ b/src/templates/core/_partials/_navbar.html @@ -21,7 +21,27 @@
- {% if navbar_recent_room_url %} + {% if 'page-my-sea' in page_class %} + {# Sprint 6 iter 6b — on any my-sea page (landing/picker or #} + {# the gatekeeper itself), CONT GAME swaps for GATE VIEW so #} + {# the user can always reach the token-deposit gatekeeper #} + {# (regardless of quota state). ` + {% elif navbar_recent_room_url %}