diff --git a/src/apps/applets/migrations/0005_gameboard_applet_heights.py b/src/apps/applets/migrations/0005_gameboard_applet_heights.py new file mode 100644 index 0000000..ae5044a --- /dev/null +++ b/src/apps/applets/migrations/0005_gameboard_applet_heights.py @@ -0,0 +1,24 @@ +from django.db import migrations + + +def increase_gameboard_applet_heights(apps, schema_editor): + Applet = apps.get_model('applets', 'Applet') + Applet.objects.filter(slug__in=['new-game', 'game-kit', 'wallet-payment']).update(grid_rows=3) + + +def revert_gameboard_applet_heights(apps, schema_editor): + Applet = apps.get_model('applets', 'Applet') + Applet.objects.filter(slug__in=['new-game', 'game-kit', 'wallet-payment']).update(grid_rows=2) + + +class Migration(migrations.Migration): + dependencies = [ + ('applets', '0004_rename_list_applet_slugs') + ] + + operations = [ + migrations.RunPython( + increase_gameboard_applet_heights, + revert_gameboard_applet_heights, + ) + ] diff --git a/src/apps/dashboard/static/apps/dashboard/dashboard.js b/src/apps/dashboard/static/apps/dashboard/dashboard.js index 3afcf39..6510edd 100644 --- a/src/apps/dashboard/static/apps/dashboard/dashboard.js +++ b/src/apps/dashboard/static/apps/dashboard/dashboard.js @@ -8,3 +8,27 @@ const initialize = (inputSelector) => { textInput.classList.remove("is-invalid"); }; }; + +const bindPaletteForms = () => { + document.querySelectorAll('form[action*="set_palette"]').forEach(form => { + form.addEventListener("submit", async (e) => { + e.preventDefault(); + const resp = await fetch(form.action, { + method: "POST", + headers: { "Accept": "application/json" }, + body: new FormData(form, e.submitter), + }); + if (!resp.ok) return; + const { palette } = await resp.json(); + // Swap body palette class + [...document.body.classList] + .filter(c => c.startsWith("palette-")) + .forEach(c => document.body.classList.remove(c)); + document.body.classList.add(palette); + // Update active swatch indicator + document.querySelectorAll(".swatch").forEach(sw => { + sw.classList.toggle("active", sw.classList.contains(palette)); + }); + }); + }); +}; diff --git a/src/apps/dashboard/tests/integrated/test_views.py b/src/apps/dashboard/tests/integrated/test_views.py index c0e2bc1..5ff2833 100644 --- a/src/apps/dashboard/tests/integrated/test_views.py +++ b/src/apps/dashboard/tests/integrated/test_views.py @@ -299,6 +299,24 @@ class SetPaletteTest(TestCase): response = self.client.post("/dashboard/set_palette", data={"palette": "palette-default"}) self.assertRedirects(response, "/", fetch_redirect_response=False) + def test_set_palette_returns_json_when_requested(self): + response = self.client.post( + "/dashboard/set_palette", + data={"palette": "palette-sepia"}, + headers={"Accept": "application/json"}, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"palette": "palette-sepia"}) + + def test_locked_palette_returns_unchanged_json(self): + response = self.client.post( + "/dashboard/set_palette", + data={"palette": "palette-nirvana"}, + headers={"Accept": "application/json"}, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"palette": "palette-default"}) + def test_dashboard_contains_set_palette_form(self): response = self.client.get(self.url) parsed = lxml.html.fromstring(response.content) diff --git a/src/apps/dashboard/views.py b/src/apps/dashboard/views.py index a8b7b9d..c7d766f 100644 --- a/src/apps/dashboard/views.py +++ b/src/apps/dashboard/views.py @@ -122,6 +122,8 @@ def set_palette(request): if palette in UNLOCKED_PALETTES: request.user.palette = palette request.user.save(update_fields=["palette"]) + if "application/json" in request.headers.get("Accept", ""): + return JsonResponse({"palette": request.user.palette}) return redirect("home") @login_required(login_url="/") diff --git a/src/functional_tests/test_applet_palette.py b/src/functional_tests/test_applet_palette.py index 36e4aad..66e043f 100644 --- a/src/functional_tests/test_applet_palette.py +++ b/src/functional_tests/test_applet_palette.py @@ -1,10 +1,49 @@ from selenium.webdriver.common.by import By +from apps.applets.models import Applet from apps.lyric.models import User from .base import FunctionalTest +class PaletteSwapTest(FunctionalTest): + def test_selecting_palette_updates_body_class_without_page_reload(self): + Applet.objects.get_or_create(slug="palette", defaults={ + "name": "Palette", "context": "dashboard", + }) + user, _ = User.objects.get_or_create(email="swap@test.io") + self.create_pre_authenticated_session("swap@test.io") + self.browser.get(self.live_server_url) + + body = self.browser.find_element(By.TAG_NAME, "body") + self.assertIn("palette-default", body.get_attribute("class")) + + # Mark the window — this survives JS execution but is wiped on a real reload + self.browser.execute_script("window._no_reload_marker = true;") + + # Click OK on a non-active palette + btn = self.wait_for( + lambda: self.browser.find_element( + By.CSS_SELECTOR, + ".palette-item:has(.swatch:not(.active)) .btn-confirm", + ) + ) + btn.click() + + # Body palette class swaps without reload + self.wait_for( + lambda: self.assertNotIn( + "palette-default", + self.browser.find_element(By.TAG_NAME, "body").get_attribute("class"), + ) + ) + + # Marker still present — no full page reload occurred + self.assertTrue( + self.browser.execute_script("return window._no_reload_marker === true;") + ) + + class SiteThemeTest(FunctionalTest): def test_page_renders_with_earthman_palette(self): self.browser.get(self.live_server_url) diff --git a/src/static_src/scss/_applets.scss b/src/static_src/scss/_applets.scss index a43142c..32333bd 100644 --- a/src/static_src/scss/_applets.scss +++ b/src/static_src/scss/_applets.scss @@ -159,9 +159,38 @@ ; background-color: rgba(0, 0, 0, 0.125); border-radius: 0.75rem; - padding: 1rem; + position: relative; + padding: 0.75rem 0.75rem 0.75rem 2rem; overflow: hidden; min-width: 0; + + > h2 { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + writing-mode: vertical-rl; + transform: rotate(180deg); + font-size: 1rem; + letter-spacing: 0.2em; + margin: 0; + padding-right: 0.2rem; + color: rgba(var(--secUser), 1); + background-color: rgba(0, 0, 0, 0.125); + box-shadow: + 0 0 0.5rem rgba(var(--priUser), 0.5), + 0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25), + ; + white-space: nowrap; + overflow: hidden; + z-index: 1; + + a { color: rgba(var(--terUser), 1); text-decoration: none; } + } grid-column: span var(--applet-cols, 12); grid-row: span var(--applet-rows, 3); diff --git a/src/static_src/scss/_dashboard.scss b/src/static_src/scss/_dashboard.scss index 56eb1f7..50dbf10 100644 --- a/src/static_src/scss/_dashboard.scss +++ b/src/static_src/scss/_dashboard.scss @@ -31,11 +31,9 @@ body.page-dashboard { #id_applets_container { #id_applet_my_notes { - padding: 1.25rem 1.5rem; display: flex; flex-direction: column; - h2 { flex-shrink: 0; margin-bottom: 0.25rem; } .my-notes-container { flex: 1; @@ -61,12 +59,10 @@ body.page-dashboard { display: flex; flex-direction: column; gap: 0.25rem; - - h2 { flex-shrink: 0; margin-bottom: 0; } } #id_applet_palette { - padding: 0; + padding: 0 0 0 1.5rem; .palette-scroll { display: flex; diff --git a/src/static_src/scss/_wallet-tokens.scss b/src/static_src/scss/_wallet-tokens.scss index 6692042..e154a50 100644 --- a/src/static_src/scss/_wallet-tokens.scss +++ b/src/static_src/scss/_wallet-tokens.scss @@ -101,11 +101,6 @@ body.page-wallet { flex-direction: column; overflow: visible; - h2 { - flex-shrink: 0; - margin-bottom: 0; - } - .token-row { flex: 1; display: flex; diff --git a/src/templates/apps/billboard/billboard.html b/src/templates/apps/billboard/billboard.html index 940be24..aaa604c 100644 --- a/src/templates/apps/billboard/billboard.html +++ b/src/templates/apps/billboard/billboard.html @@ -5,14 +5,14 @@ {% block content %}
-

My Games

+

My Scrolls

diff --git a/src/templates/apps/dashboard/_partials/_applet-my-notes.html b/src/templates/apps/dashboard/_partials/_applet-my-notes.html index 1c591bf..f36e84d 100644 --- a/src/templates/apps/dashboard/_partials/_applet-my-notes.html +++ b/src/templates/apps/dashboard/_partials/_applet-my-notes.html @@ -2,7 +2,7 @@ id="id_applet_my_notes" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" > -

My notes

+

My Notes