My Sea iter 4b polish: Brief banner uses standard portaled .note-banner (Gaussian glass atop h2); next-free-draw datetime in dedicated <time> slot (not "Invalid Date"); DEL guard reuses shared #id_guard_portal from base.html — TDD
UX refactor on top of iter 4b (b76d3c5) per user direction:
(1) Brief banner — replaced custom `.my-sea-brief` markup + SCSS w. a call to `Brief.showBanner` from note.js. Now matches the my-notes / my-sign default-deck-warning Briefs exactly: standard `.note-banner` portaled atop the h2 w. Gaussian-glass backdrop-filter blur. Tagged `.my-sea-locked-banner` for FT disambiguation only — no visual override.
(2) Brief timestamp — fix for "Invalid Date" rendering in note.js's `<time class="note-banner__timestamp">` slot. Previously passed `created_at: ''` to `Brief.showBanner` → `new Date('')` returns Invalid Date → `toLocaleDateString` renders "Invalid Date". Now passes the next-free-draw ISO timestamp as `created_at` (server emits via `|date:'c'`). After Brief.showBanner returns, the `_showFreeDrawLockedBrief` JS overwrites the rendered text w. the more detailed `D, M j @ g:i A` format ("Wed, May 20 @ 11:57 PM") — leaves the ISO `datetime=` attribute intact for accessibility. The `line_text` no longer carries the timestamp inline (it's redundant w. the dedicated slot).
(3) DEL guard portal — replaced custom `#id_my_sea_del_portal` fullscreen modal + `.my-sea-del-portal` SCSS w. a call to `window.showGuard` from base.html, targeting the shared `#id_guard_portal`. Same Gaussian-glass tooltip the room gear-menu DEL flow uses: no backdrop, positioned above the anchor button, standard `.btn-confirm OK` + `.btn-cancel NVM` pair. Bundled a non-breaking `options.yesLabel` extension to `show()` in base.html for future destructive flows that need a custom YES label (defaults to 'OK', resets on dismiss/confirm) — my-sea doesn't use it per user direction (the `.btn-confirm` class implies "OK"; destructive intent belongs on the trigger button, which is `.btn-danger DEL`).
Tests: 30 iter-4b ITs (model + lock + delete + saved-draw view branches) + 5 iter-4b FTs all green; IT/FT assertions updated to target the shared portal markup (`#id_guard_portal.active`, `.guard-yes`, `.guard-no`, `.note-banner.my-sea-locked-banner`).
Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1116,26 +1116,45 @@ class MySeaViewWithSavedDrawTest(TestCase):
|
|||||||
self.assertNotIn("my-sea-sign-gate", html)
|
self.assertNotIn("my-sea-sign-gate", html)
|
||||||
self.assertIn('data-phase="picker"', html)
|
self.assertIn('data-phase="picker"', html)
|
||||||
|
|
||||||
def test_view_renders_brief_banner_when_active_draw_exists(self):
|
def test_view_triggers_brief_banner_when_active_draw_exists(self):
|
||||||
|
# Brief is rendered client-side via Brief.showBanner (standard
|
||||||
|
# `.note-banner` w. Gaussian-glass bg, portaled atop the h2 —
|
||||||
|
# same UX as my-notes / my-sign default-deck-warning Briefs).
|
||||||
|
# Server emits a `window._showFreeDrawLockedBrief("<iso>")` call
|
||||||
|
# gated on active_draw; ISO timestamp (`|date:'c'`) is re-used
|
||||||
|
# as both `created_at` AND the source for the human-formatted
|
||||||
|
# display string note.js renders in the `.note-banner__timestamp`
|
||||||
|
# slot — single source of truth, no "Invalid Date" on bad input.
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
self.assertContains(response, "my-sea-brief")
|
# Match the call form w. opening quote — the bare token
|
||||||
self.assertContains(response, "my-sea-brief__timestamp")
|
# `_showFreeDrawLockedBrief(` also appears in the function
|
||||||
self.assertContains(response, "my-sea-brief__nvm")
|
# definition emitted unconditionally inside the picker IIFE.
|
||||||
|
self.assertContains(response, 'window._showFreeDrawLockedBrief("')
|
||||||
|
# The ISO format produced by Django's `|date:'c'` starts with the
|
||||||
|
# full year + ISO-style T separator — pin a representative token.
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
expected_year = (timezone.now() + timedelta(hours=24)).strftime("%Y")
|
||||||
|
self.assertContains(response, '_showFreeDrawLockedBrief("' + expected_year)
|
||||||
|
|
||||||
def test_brief_banner_hidden_without_active_draw(self):
|
def test_view_does_not_trigger_brief_banner_without_active_draw(self):
|
||||||
# Markup is rendered unconditionally so JS can un-hide it on LOCK
|
# Definition of `_showFreeDrawLockedBrief` is always emitted;
|
||||||
# HAND POST success without a page reload. When no active_draw,
|
# only the CALL is gated on active_draw. Pin the call form.
|
||||||
# the wrapping div carries `[hidden]` so the banner is invisible.
|
|
||||||
from apps.gameboard.models import MySeaDraw
|
from apps.gameboard.models import MySeaDraw
|
||||||
MySeaDraw.objects.all().delete()
|
MySeaDraw.objects.all().delete()
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
self.assertContains(response, '<div class="my-sea-brief" hidden>')
|
self.assertNotContains(response, 'window._showFreeDrawLockedBrief("')
|
||||||
|
|
||||||
def test_view_renders_del_guard_portal_when_active_draw_exists(self):
|
def test_view_wires_del_button_to_shared_guard_portal_when_active_draw(self):
|
||||||
|
# No my-sea-specific guard markup — the picker IIFE calls
|
||||||
|
# `window.showGuard(delBtn, "Are you sure?", confirmFn)` which
|
||||||
|
# targets the shared #id_guard_portal from base.html (same
|
||||||
|
# tooltip the room gear-menu uses; standard OK/NVM button pair).
|
||||||
|
# Server emits the call site; we pin the call form + the delete
|
||||||
|
# URL it POSTs to.
|
||||||
response = self.client.get(reverse("my_sea"))
|
response = self.client.get(reverse("my_sea"))
|
||||||
self.assertContains(response, 'id="id_my_sea_del_portal"')
|
self.assertContains(response, "window.showGuard(")
|
||||||
self.assertContains(response, "my-sea-del-portal__confirm")
|
self.assertContains(response, reverse("my_sea_delete"))
|
||||||
self.assertContains(response, "my-sea-del-portal__nvm")
|
|
||||||
|
|
||||||
def test_saved_hand_renders_as_filled_slots_in_picker(self):
|
def test_saved_hand_renders_as_filled_slots_in_picker(self):
|
||||||
# Each saved position's slot is server-rendered as `--filled` w.
|
# Each saved position's slot is server-rendered as `--filled` w.
|
||||||
|
|||||||
@@ -1096,35 +1096,45 @@ class MySeaLockHandTest(FunctionalTest):
|
|||||||
# ── Test 3 ───────────────────────────────────────────────────────────────
|
# ── Test 3 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_saved_draw_renders_brief_banner_with_next_free_draw_timestamp(self):
|
def test_saved_draw_renders_brief_banner_with_next_free_draw_timestamp(self):
|
||||||
"""Post-lock UX: a Look!-formatted Brief banner above the picker
|
"""Post-lock UX: a Look!-formatted Brief banner appears atop the
|
||||||
informs the user when the next free draw is available + offers a
|
h2 (standard portaled `.note-banner` w. Gaussian-glass bg, same
|
||||||
NVM button to dismiss. Mirrors the Brief banner shape from the
|
styling as my-notes / my-sign default-deck-warning Briefs). The
|
||||||
Baltimorean Note unlock + the my-sign default-deck warning."""
|
next-free-draw timestamp lives in the dedicated `.note-banner__
|
||||||
|
timestamp` `<time>` slot (note.js's standard datetime element),
|
||||||
|
formatted by JS to `D, M j @ g:i A` shape — e.g. "Wed, May 20 @
|
||||||
|
11:57 PM". Tagged `.my-sea-locked-banner` so this FT disambiguates
|
||||||
|
from any other Briefs that may stack on the page."""
|
||||||
self._save_draw_for_user()
|
self._save_draw_for_user()
|
||||||
self.create_pre_authenticated_session(self.email)
|
self.create_pre_authenticated_session(self.email)
|
||||||
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
||||||
brief = self.wait_for(
|
brief = self.wait_for(
|
||||||
lambda: self.browser.find_element(
|
lambda: self.browser.find_element(
|
||||||
By.CSS_SELECTOR, ".my-sea-brief"
|
By.CSS_SELECTOR, ".note-banner.my-sea-locked-banner"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
text = brief.text
|
text = brief.text
|
||||||
self.assertIn("Look!", text)
|
self.assertIn("Look!", text)
|
||||||
self.assertIn("free draw", text.lower())
|
self.assertIn("free draw", text.lower())
|
||||||
# The timestamp is rendered inside a dedicated child so the JS
|
# Timestamp slot owns the next-free-draw datetime. The "@" token
|
||||||
# NVM-dismiss handler can find + style it independently of the
|
# in the `D, M j @ g:i A` format is a stable assertion target;
|
||||||
# surrounding copy.
|
# also pin the year to confirm the source ISO parsed correctly
|
||||||
ts = brief.find_element(By.CSS_SELECTOR, ".my-sea-brief__timestamp")
|
# (would render "Invalid Date" if note.js got an empty string).
|
||||||
self.assertTrue(ts.text.strip(), "brief should render a non-empty next-free-draw timestamp")
|
ts = brief.find_element(By.CSS_SELECTOR, ".note-banner__timestamp")
|
||||||
# NVM button is present (Jasmine pins the dismiss-on-click).
|
ts_text = ts.text
|
||||||
brief.find_element(By.CSS_SELECTOR, ".my-sea-brief__nvm")
|
self.assertIn("@", ts_text)
|
||||||
|
self.assertNotIn("Invalid", ts_text)
|
||||||
|
# NVM dismiss button is wired by note.js itself.
|
||||||
|
brief.find_element(By.CSS_SELECTOR, ".note-banner__nvm")
|
||||||
|
|
||||||
# ── Test 4 ───────────────────────────────────────────────────────────────
|
# ── Test 4 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_del_click_opens_guard_portal_with_uniform_confirm_copy(self):
|
def test_del_click_opens_shared_guard_portal(self):
|
||||||
"""DEL on a locked hand opens `#id_my_sea_del_portal` — uniform
|
"""DEL on a locked hand opens the shared `#id_guard_portal` from
|
||||||
'Are you sure?' copy (no conditional quota wording; the Brief
|
base.html (same Gaussian-glass tooltip the room gear-menu uses)
|
||||||
banner carries that info separately) w. CONFIRM + NVM buttons."""
|
w. uniform 'Are you sure?' copy + the standard `.btn-confirm OK`
|
||||||
|
+ `.btn-cancel NVM` button pair. The Brief banner above carries
|
||||||
|
the quota-specific info, so the portal stays text-free of
|
||||||
|
conditional wording."""
|
||||||
self._save_draw_for_user()
|
self._save_draw_for_user()
|
||||||
self.create_pre_authenticated_session(self.email)
|
self.create_pre_authenticated_session(self.email)
|
||||||
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
self.browser.get(self.live_server_url + "/gameboard/my-sea/")
|
||||||
@@ -1137,22 +1147,21 @@ class MySeaLockHandTest(FunctionalTest):
|
|||||||
delbtn.click()
|
delbtn.click()
|
||||||
portal = self.wait_for(
|
portal = self.wait_for(
|
||||||
lambda: self.browser.find_element(
|
lambda: self.browser.find_element(
|
||||||
By.CSS_SELECTOR, "#id_my_sea_del_portal"
|
By.CSS_SELECTOR, "#id_guard_portal.active"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.wait_for(lambda: self.assertTrue(portal.is_displayed()))
|
self.wait_for(lambda: self.assertTrue(portal.is_displayed()))
|
||||||
text = portal.text.lower()
|
self.assertIn("sure", portal.text.lower())
|
||||||
self.assertIn("sure", text)
|
portal.find_element(By.CSS_SELECTOR, ".guard-yes")
|
||||||
portal.find_element(By.CSS_SELECTOR, ".my-sea-del-portal__confirm")
|
portal.find_element(By.CSS_SELECTOR, ".guard-no")
|
||||||
portal.find_element(By.CSS_SELECTOR, ".my-sea-del-portal__nvm")
|
|
||||||
|
|
||||||
# ── Test 5 ───────────────────────────────────────────────────────────────
|
# ── Test 5 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def test_del_confirm_clears_saved_draw_and_returns_to_landing(self):
|
def test_del_confirm_clears_saved_draw_and_returns_to_landing(self):
|
||||||
"""Clicking the portal's CONFIRM POSTs to the delete endpoint
|
"""Clicking the portal's OK (`.guard-yes`) POSTs to the delete
|
||||||
→ server wipes the MySeaDraw row → reload lands on the FREE DRAW
|
endpoint → server wipes the MySeaDraw row → reload lands on the
|
||||||
landing again (no saved hand, no Brief banner, FREE DRAW btn
|
FREE DRAW landing again (no saved hand, no Brief banner, FREE
|
||||||
present)."""
|
DRAW btn present)."""
|
||||||
from apps.gameboard.models import MySeaDraw
|
from apps.gameboard.models import MySeaDraw
|
||||||
self._save_draw_for_user()
|
self._save_draw_for_user()
|
||||||
self.assertEqual(MySeaDraw.objects.filter(user=self.gamer).count(), 1)
|
self.assertEqual(MySeaDraw.objects.filter(user=self.gamer).count(), 1)
|
||||||
@@ -1166,7 +1175,7 @@ class MySeaLockHandTest(FunctionalTest):
|
|||||||
picker.find_element(By.CSS_SELECTOR, "#id_sea_del").click()
|
picker.find_element(By.CSS_SELECTOR, "#id_sea_del").click()
|
||||||
confirm = self.wait_for(
|
confirm = self.wait_for(
|
||||||
lambda: self.browser.find_element(
|
lambda: self.browser.find_element(
|
||||||
By.CSS_SELECTOR, ".my-sea-del-portal__confirm"
|
By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
confirm.click()
|
confirm.click()
|
||||||
|
|||||||
@@ -505,74 +505,9 @@ body.page-gameboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Iter 4b: Brief banner + DEL guard portal ─────────────────────────────────
|
// ── Iter 4b: Brief banner + DEL guard portal ─────────────────────────────────
|
||||||
|
// Both reuse shared chrome: the Brief is `.note-banner` from note.js
|
||||||
// Brief banner — Look!-formatted strip above the picker whenever a saved
|
// (portaled atop h2 w. Gaussian glass); the DEL guard is `#id_guard_portal`
|
||||||
// draw occupies the user's free-quota slot. Shape mirrors .my-sea-sign-
|
// from base.html (the same one the room gear-menu DEL uses, positioned
|
||||||
// gate but sized as a banner (full-width, single line + actions row).
|
// above the anchor button w. Gaussian glass + no backdrop). The picker IIFE
|
||||||
.my-sea-brief {
|
// invokes it via `window.showGuard(delBtn, "Are you sure?", confirmFn,
|
||||||
display: flex;
|
// null, {yesLabel: "DEL"})`. No my-sea-specific SCSS needed.
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
margin: 0.5rem 0 1rem;
|
|
||||||
background-color: rgba(var(--secUser), 0.65);
|
|
||||||
border: 0.1rem solid rgba(var(--terUser), 0.6);
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
color: rgba(var(--terUser), 1);
|
|
||||||
font-size: 0.95rem;
|
|
||||||
|
|
||||||
&[hidden] { display: none; }
|
|
||||||
|
|
||||||
.my-sea-brief__line {
|
|
||||||
margin: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-sea-brief__timestamp {
|
|
||||||
font-weight: bold;
|
|
||||||
color: rgba(var(--ninUser), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-sea-brief__nvm {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DEL guard portal — fixed-position centered modal w. a uniform
|
|
||||||
// 'Are you sure?' prompt. CONFIRM POSTs to /gameboard/my-sea/delete;
|
|
||||||
// NVM closes the portal. The Brief banner above carries the quota-
|
|
||||||
// specific copy so this stays free of conditional text.
|
|
||||||
.my-sea-del-portal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 1100;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
|
|
||||||
&[hidden] { display: none; }
|
|
||||||
|
|
||||||
.my-sea-del-portal__panel {
|
|
||||||
background-color: rgba(var(--secUser), 0.95);
|
|
||||||
border: 0.15rem solid rgba(var(--terUser), 0.8);
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 1.25rem 1.75rem;
|
|
||||||
min-width: 18rem;
|
|
||||||
text-align: center;
|
|
||||||
color: rgba(var(--terUser), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-sea-del-portal__line {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.my-sea-del-portal__actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -205,39 +205,12 @@
|
|||||||
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
{% include "apps/gameboard/_partials/_sea_stage.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Iter 4b — Look!-formatted Brief banner above the picker. #}
|
{# Iter 4b — DEL guard reuses the shared `#id_guard_portal` #}
|
||||||
{# Always rendered when the picker is rendered, but hidden #}
|
{# from base.html (the same one the room's gear-menu DEL btn #}
|
||||||
{# unless a saved draw occupies the user's free-quota slot #}
|
{# uses). Gaussian-glass tooltip positioned above the DEL btn,#}
|
||||||
{# (server) OR LOCK HAND just fired (client un-hides on the #}
|
{# no backdrop. The picker IIFE below invokes it via #}
|
||||||
{# fetch response w. the next-free-draw timestamp). Avoiding #}
|
{# `window.showGuard(delBtn, "Are you sure?", confirmFn, null,#}
|
||||||
{# a full page reload on LOCK lets the iter-4a FTs keep their #}
|
{# {yesLabel: "DEL"})` when DEL is clicked post-lock. #}
|
||||||
{# picker element refs valid post-lock. #}
|
|
||||||
<div class="my-sea-brief"{% if not active_draw %} hidden{% endif %}>
|
|
||||||
<p class="my-sea-brief__line">
|
|
||||||
Look!—your free draw is locked in for the next 24 hours. Next free draw available at
|
|
||||||
<time class="my-sea-brief__timestamp"
|
|
||||||
datetime="{% if next_free_draw_at %}{{ next_free_draw_at|date:'c' }}{% endif %}">{% if next_free_draw_at %}{{ next_free_draw_at|date:'D, M j @ g:i A' }}{% endif %}</time>.
|
|
||||||
</p>
|
|
||||||
<button type="button" class="btn btn-cancel my-sea-brief__nvm">NVM</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Iter 4b — DEL guard portal. Uniform 'Are you sure?' copy #}
|
|
||||||
{# regardless of quota state (the Brief banner above carries #}
|
|
||||||
{# the quota-specific info). Always rendered (hidden by #}
|
|
||||||
{# default); DEL click un-hides when picker is `_locked`. #}
|
|
||||||
{# CONFIRM POSTs to /gameboard/my-sea/delete; NVM dismisses. #}
|
|
||||||
<div id="id_my_sea_del_portal" class="my-sea-del-portal" hidden>
|
|
||||||
<div class="my-sea-del-portal__panel">
|
|
||||||
<p class="my-sea-del-portal__line">Are you sure?</p>
|
|
||||||
<div class="my-sea-del-portal__actions">
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-cancel my-sea-del-portal__nvm">NVM</button>
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-danger my-sea-del-portal__confirm"
|
|
||||||
data-delete-url="{% url 'my_sea_delete' %}">CONFIRM</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{# Sprint 5 iter 4a — shuffled deck (levity + gravity halves, #}
|
{# Sprint 5 iter 4a — shuffled deck (levity + gravity halves, #}
|
||||||
{# sig excluded) embedded as JSON; JS reads on init and #}
|
{# sig excluded) embedded as JSON; JS reads on init and #}
|
||||||
{# pops from the relevant pile on each deposit. #}
|
{# pops from the relevant pile on each deposit. #}
|
||||||
@@ -492,19 +465,39 @@
|
|||||||
// ── DEL semantics differ by lock state ──────────────────
|
// ── DEL semantics differ by lock state ──────────────────
|
||||||
// Pre-lock: DEL resets the in-progress hand client-side
|
// Pre-lock: DEL resets the in-progress hand client-side
|
||||||
// (iter 4a behaviour — no server round-trip).
|
// (iter 4a behaviour — no server round-trip).
|
||||||
// Post-lock: DEL opens `#id_my_sea_del_portal` guard portal
|
// Post-lock: DEL invokes the shared `#id_guard_portal`
|
||||||
// (iter 4b). The portal CONFIRM POSTs to
|
// from base.html via `window.showGuard`, w. a
|
||||||
// /gameboard/my-sea/delete; NVM closes the
|
// "DEL" YES-label override (the room's gear-
|
||||||
// portal.
|
// menu DEL flow uses the same portal). On YES
|
||||||
|
// we POST to /gameboard/my-sea/delete then
|
||||||
|
// navigate back to the page (server returns
|
||||||
|
// 204; we redirect manually to land on the
|
||||||
|
// FREE DRAW landing).
|
||||||
if (delBtn) {
|
if (delBtn) {
|
||||||
delBtn.addEventListener('click', function (e) {
|
delBtn.addEventListener('click', function (e) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var portal = document.getElementById('id_my_sea_del_portal');
|
if (!_locked) {
|
||||||
if (_locked && portal) {
|
_resetHand();
|
||||||
portal.hidden = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_resetHand();
|
if (!window.showGuard) return;
|
||||||
|
// Trigger btn (DEL, `.btn-danger`) opens the shared
|
||||||
|
// guard portal; the portal's confirm button is the
|
||||||
|
// standard `.btn-confirm` "OK" + `.btn-cancel` "NVM"
|
||||||
|
// pair — matches the room gear-menu DEL flow exactly.
|
||||||
|
window.showGuard(
|
||||||
|
delBtn,
|
||||||
|
'Are you sure?',
|
||||||
|
function () {
|
||||||
|
fetch('{% url "my_sea_delete" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {'X-CSRFToken': _csrf()},
|
||||||
|
}).then(function (r) {
|
||||||
|
if (r.ok) window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (lockBtn) {
|
if (lockBtn) {
|
||||||
@@ -538,11 +531,8 @@
|
|||||||
return m ? decodeURIComponent(m[1]) : '';
|
return m ? decodeURIComponent(m[1]) : '';
|
||||||
}
|
}
|
||||||
function _formatTimestamp(iso) {
|
function _formatTimestamp(iso) {
|
||||||
// Mirror the server-side `D, M j @ g:i A` format used in
|
// Mirror the server-side `D, M j @ g:i A` format
|
||||||
// the template's pre-rendered next-free-draw timestamp
|
// (e.g., "Thu, May 21 @ 2:41 AM").
|
||||||
// (e.g., "Thu, May 21 @ 2:41 AM"). Keeps the post-LOCK
|
|
||||||
// visual consistent with a fresh-page-load saved-draw
|
|
||||||
// render.
|
|
||||||
var d = new Date(iso);
|
var d = new Date(iso);
|
||||||
if (isNaN(d)) return '';
|
if (isNaN(d)) return '';
|
||||||
var DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
var DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
||||||
@@ -556,16 +546,40 @@
|
|||||||
return DAYS[d.getDay()] + ', ' + MONTHS[d.getMonth()]
|
return DAYS[d.getDay()] + ', ' + MONTHS[d.getMonth()]
|
||||||
+ ' ' + d.getDate() + ' @ ' + h + ':' + mm + ' ' + ampm;
|
+ ' ' + d.getDate() + ' @ ' + h + ':' + mm + ' ' + ampm;
|
||||||
}
|
}
|
||||||
function _revealBrief(nextFreeDrawIso) {
|
window._showFreeDrawLockedBrief = function (iso) {
|
||||||
var brief = document.querySelector('.my-sea-brief');
|
// Standard Brief banner — portaled atop the h2 w.
|
||||||
if (!brief) return;
|
// Gaussian-glass bg (see [[note.js]] showBanner). The
|
||||||
var ts = brief.querySelector('.my-sea-brief__timestamp');
|
// next-free-draw moment is passed as an ISO string +
|
||||||
if (ts && nextFreeDrawIso) {
|
// re-used as `created_at` so note.js's `<time
|
||||||
ts.setAttribute('datetime', nextFreeDrawIso);
|
// class="note-banner__timestamp">` slot renders the
|
||||||
ts.textContent = _formatTimestamp(nextFreeDrawIso);
|
// datetime instead of "Invalid Date" (which it does
|
||||||
}
|
// for empty/invalid input). The `line_text` carries
|
||||||
brief.hidden = false;
|
// only the contextual prose now — the dedicated slot
|
||||||
|
// owns the timestamp display.
|
||||||
|
if (!window.Brief || !Brief.showBanner) return;
|
||||||
|
Brief.showBanner({
|
||||||
|
title: 'Free draw locked',
|
||||||
|
line_text:
|
||||||
|
'Look!—your free draw is locked in. ' +
|
||||||
|
'Next free draw available at:',
|
||||||
|
post_url: '{% url "gameboard" %}',
|
||||||
|
created_at: iso,
|
||||||
|
kind: 'NUDGE',
|
||||||
|
});
|
||||||
|
var banner = document.querySelector('.note-banner');
|
||||||
|
if (banner) {
|
||||||
|
banner.classList.add('my-sea-locked-banner');
|
||||||
|
// note.js renders the timestamp as `toLocaleDateString`
|
||||||
|
// (e.g., "May 20, 2026") — short-form, no time. Our
|
||||||
|
// use case wants the full `D, M j @ g:i A` shape
|
||||||
|
// (e.g., "Wed, May 20 @ 11:57 PM") so the user sees
|
||||||
|
// both the date AND the precise unlock hour. Overwrite
|
||||||
|
// the rendered text in-place (leaves the `datetime=`
|
||||||
|
// attribute intact for accessibility tooling).
|
||||||
|
var ts = banner.querySelector('.note-banner__timestamp');
|
||||||
|
if (ts && iso) ts.textContent = _formatTimestamp(iso);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
function _postLock(hand) {
|
function _postLock(hand) {
|
||||||
fetch('{% url "my_sea_lock" %}', {
|
fetch('{% url "my_sea_lock" %}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -582,49 +596,11 @@
|
|||||||
return r.ok ? r.json() : null;
|
return r.ok ? r.json() : null;
|
||||||
}).then(function (body) {
|
}).then(function (body) {
|
||||||
if (body && body.next_free_draw_at) {
|
if (body && body.next_free_draw_at) {
|
||||||
_revealBrief(body.next_free_draw_at);
|
window._showFreeDrawLockedBrief(body.next_free_draw_at);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DEL guard portal wiring ────────────────────────────
|
|
||||||
var portal = document.getElementById('id_my_sea_del_portal');
|
|
||||||
if (portal) {
|
|
||||||
var nvmBtn = portal.querySelector('.my-sea-del-portal__nvm');
|
|
||||||
var confirmBtn = portal.querySelector('.my-sea-del-portal__confirm');
|
|
||||||
if (nvmBtn) {
|
|
||||||
nvmBtn.addEventListener('click', function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
portal.hidden = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (confirmBtn) {
|
|
||||||
confirmBtn.addEventListener('click', function (e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
var url = confirmBtn.dataset.deleteUrl || '';
|
|
||||||
if (!url) return;
|
|
||||||
fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {'X-CSRFToken': _csrf()},
|
|
||||||
}).then(function (r) {
|
|
||||||
if (r.ok) window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Brief banner NVM ──────────────────────────────────
|
|
||||||
var brief = document.querySelector('.my-sea-brief');
|
|
||||||
if (brief) {
|
|
||||||
var briefNvm = brief.querySelector('.my-sea-brief__nvm');
|
|
||||||
if (briefNvm) {
|
|
||||||
briefNvm.addEventListener('click', function () {
|
|
||||||
brief.hidden = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncLabels(spread) {
|
function syncLabels(spread) {
|
||||||
var labels = POSITION_LABELS[spread] || {};
|
var labels = POSITION_LABELS[spread] || {};
|
||||||
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
|
cross.querySelectorAll('.sea-pos-label').forEach(function (el) {
|
||||||
@@ -731,6 +707,22 @@
|
|||||||
{# Tagged w. .my-sea-intro-banner so FTs disambiguate from #}
|
{# Tagged w. .my-sea-intro-banner so FTs disambiguate from #}
|
||||||
{# any other Briefs on the page. #}
|
{# any other Briefs on the page. #}
|
||||||
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
<script src="{% static 'apps/dashboard/note.js' %}"></script>
|
||||||
|
{% if active_draw %}
|
||||||
|
{# Iter 4b — saved-draw Brief. Standard portaled banner via #}
|
||||||
|
{# Brief.showBanner (Gaussian-glass bg, atop-h2 positioning); #}
|
||||||
|
{# the on-LOCK-success path inside the picker IIFE calls the #}
|
||||||
|
{# same `window._showFreeDrawLockedBrief` so a freshly-locked #}
|
||||||
|
{# hand gets the identical UX without a page reload. Pass an #}
|
||||||
|
{# ISO timestamp (`|date:'c'`) so note.js's `<time>` slot #}
|
||||||
|
{# parses cleanly instead of rendering "Invalid Date". #}
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
if (window._showFreeDrawLockedBrief) {
|
||||||
|
window._showFreeDrawLockedBrief("{{ next_free_draw_at|date:'c' }}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
{% if show_backup_intro_banner %}
|
{% if show_backup_intro_banner %}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
@@ -141,6 +141,10 @@
|
|||||||
_cb = callback;
|
_cb = callback;
|
||||||
_onDismiss = onDismiss || null;
|
_onDismiss = onDismiss || null;
|
||||||
portal.querySelector('.guard-message').innerHTML = message;
|
portal.querySelector('.guard-message').innerHTML = message;
|
||||||
|
// Optional override for the YES-button label (e.g., "DEL" for
|
||||||
|
// a destructive-named action). Resets to "OK" inside dismiss/
|
||||||
|
// doConfirm so the next show() starts from the default.
|
||||||
|
portal.querySelector('.guard-yes').textContent = options.yesLabel || 'OK';
|
||||||
portal.classList.add('active');
|
portal.classList.add('active');
|
||||||
var rect = anchor.getBoundingClientRect();
|
var rect = anchor.getBoundingClientRect();
|
||||||
var pw = portal.offsetWidth;
|
var pw = portal.offsetWidth;
|
||||||
@@ -165,6 +169,7 @@
|
|||||||
if (!portal) return;
|
if (!portal) return;
|
||||||
var od = _onDismiss;
|
var od = _onDismiss;
|
||||||
portal.classList.remove('active');
|
portal.classList.remove('active');
|
||||||
|
portal.querySelector('.guard-yes').textContent = 'OK';
|
||||||
_cb = null;
|
_cb = null;
|
||||||
_onDismiss = null;
|
_onDismiss = null;
|
||||||
if (od) od();
|
if (od) od();
|
||||||
@@ -173,6 +178,7 @@
|
|||||||
function doConfirm() {
|
function doConfirm() {
|
||||||
var cb = _cb;
|
var cb = _cb;
|
||||||
portal.classList.remove('active');
|
portal.classList.remove('active');
|
||||||
|
portal.querySelector('.guard-yes').textContent = 'OK';
|
||||||
_cb = null;
|
_cb = null;
|
||||||
_onDismiss = null;
|
_onDismiss = null;
|
||||||
if (cb) cb();
|
if (cb) cb();
|
||||||
|
|||||||
Reference in New Issue
Block a user