Compare commits
2 Commits
97601586c5
...
382dd5958f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
382dd5958f | ||
|
|
47d84b6bf2 |
23
src/apps/applets/static/apps/scripts/applets.js
Normal file
23
src/apps/applets/static/apps/scripts/applets.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const initGearMenus = () => {
|
||||
document.querySelectorAll('.gear-btn').forEach(gear => {
|
||||
const menuId = gear.dataset.menuTarget;
|
||||
|
||||
gear.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const menu = document.getElementById(menuId);
|
||||
if (!menu) return;
|
||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const menu = document.getElementById(menuId);
|
||||
if (!menu || menu.style.display === 'none') return;
|
||||
if (e.target.closest('.applet-menu-cancel') || !menu.contains(e.target)) {
|
||||
menu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initGearMenus);
|
||||
@@ -2,6 +2,7 @@ from django.db.utils import IntegrityError
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.applets.models import Applet, UserApplet
|
||||
from apps.applets.utils import applet_context
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
@@ -25,6 +26,7 @@ class AppletModelTest(TestCase):
|
||||
self.assertEqual(self.applet.grid_cols, 12)
|
||||
self.assertEqual(self.applet.grid_rows, 3)
|
||||
|
||||
|
||||
class UserAppletModelTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="a@b.cde")
|
||||
@@ -37,4 +39,27 @@ class UserAppletModelTest(TestCase):
|
||||
def test_user_applet_unique_per_user_and_applet(self):
|
||||
UserApplet.objects.create(user=self.user, applet=self.applet, visible=True)
|
||||
with self.assertRaises(IntegrityError):
|
||||
UserApplet.objects.create(user=self.user, applet=self.applet, visible=False)
|
||||
UserApplet.objects.create(user=self.user, applet=self.applet, visible=False)
|
||||
|
||||
class AppletContextTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="a@b.cde")
|
||||
self.dash_applet = Applet.objects.create(slug="username", name="Username", context="dashboard")
|
||||
self.game_applet = Applet.objects.create(slug="new-game", name="New Game", context="gameboard")
|
||||
|
||||
def test_filters_by_context(self):
|
||||
result = applet_context(self.user, "dashboard")
|
||||
slugs = [e["applet"].slug for e in result]
|
||||
self.assertIn("username", slugs)
|
||||
self.assertNotIn("new-game", slugs)
|
||||
|
||||
def test_defaults_to_applet_default_visible(self):
|
||||
result = applet_context(self.user, "dashboard")
|
||||
[entry] = [e for e in result if e["applet"].slug == "username"]
|
||||
self.assertTrue(entry["visible"])
|
||||
|
||||
def test_respects_user_applet_visible_false(self):
|
||||
UserApplet.objects.create(user=self.user, applet=self.dash_applet, visible=False)
|
||||
result = applet_context(self.user, "dashboard")
|
||||
[entry] = [e for e in result if e["applet"].slug == "username"]
|
||||
self.assertFalse(entry["visible"])
|
||||
|
||||
11
src/apps/applets/utils.py
Normal file
11
src/apps/applets/utils.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from apps.applets.models import Applet, UserApplet
|
||||
|
||||
|
||||
def applet_context(user, context):
|
||||
ua_map = {ua.applet_id: ua.visible for ua in user.user_applets.all()}
|
||||
applets = {a.slug: a for a in Applet.objects.filter(context=context)}
|
||||
return [
|
||||
{"applet": applets[slug], "visible": ua_map.get(applets[slug].pk, applets[slug].default_visible)}
|
||||
for slug in applets
|
||||
if slug in applets
|
||||
]
|
||||
@@ -8,23 +8,3 @@ const initialize = (inputSelector) => {
|
||||
textInput.classList.remove("is-invalid");
|
||||
};
|
||||
};
|
||||
|
||||
const initGearMenu = () => {
|
||||
const gear = document.getElementById('id_dash_gear');
|
||||
if (!gear) return;
|
||||
|
||||
gear.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const menu = document.getElementById('id_applet_menu');
|
||||
if (!menu) return;
|
||||
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const menu = document.getElementById('id_applet_menu');
|
||||
if (!menu || menu.style.display === 'none') return;
|
||||
if (e.target.closest('#id_applet_menu_cancel') || !menu.contains(e.target)) {
|
||||
menu.style.display = 'none';
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -66,6 +66,7 @@ class NewListTest(TestCase):
|
||||
response = self.post_invalid_input()
|
||||
self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))
|
||||
|
||||
@override_settings(COMPRESS_ENABLED=False)
|
||||
class ListViewTest(TestCase):
|
||||
def test_uses_list_template(self):
|
||||
mylist = List.objects.create()
|
||||
@@ -358,7 +359,7 @@ class ProfileViewTest(TestCase):
|
||||
[username_input] = parsed.cssselect("#id_new_username")
|
||||
self.assertEqual("discoman", username_input.get("value"))
|
||||
|
||||
class ToggleAppletsViewTest(TestCase):
|
||||
class ToggleDashAppletsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="disco@test.io")
|
||||
self.client.force_login(self.user)
|
||||
@@ -402,6 +403,13 @@ class ToggleAppletsViewTest(TestCase):
|
||||
self.assertEqual(len(parsed.cssselect("#id_applet_username")), 1)
|
||||
self.assertEqual(len(parsed.cssselect("#id_applet_palette")), 0)
|
||||
|
||||
def test_toggle_applets_does_not_affect_gameboard_applets(self):
|
||||
game_applet, _ = Applet.objects.get_or_create(
|
||||
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
|
||||
)
|
||||
self.client.post(self.url, {"applets": ["username", "palette"]})
|
||||
self.assertFalse(UserApplet.objects.filter(user=self.user, applet=game_applet). exists())
|
||||
|
||||
class AppletVisibilityContextTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="disco@test.io")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import lxml.html
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test import override_settings, TestCase
|
||||
|
||||
from apps.lyric.models import Token, User, Wallet
|
||||
|
||||
|
||||
@override_settings(COMPRESS_ENABLED=False)
|
||||
class WalletViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="capman@test.io")
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.shortcuts import redirect, render
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
from apps.applets.models import Applet, UserApplet
|
||||
from apps.applets.utils import applet_context
|
||||
from apps.dashboard.forms import ExistingListItemForm, ItemForm
|
||||
from apps.dashboard.models import Item, List
|
||||
from apps.lyric.models import PaymentMethod, Token, User, Wallet
|
||||
@@ -36,16 +37,6 @@ def _recent_lists(user, limit=3):
|
||||
.distinct()[:limit]
|
||||
)
|
||||
|
||||
def _applet_context(user):
|
||||
ua_map = {ua.applet_id: ua.visible for ua in user.user_applets.all()}
|
||||
applets = {a.slug: a for a in Applet.objects.all()}
|
||||
return [
|
||||
{"applet": applets[slug], "visible": ua_map.get(applets[slug].pk, applets[slug].default_visible)}
|
||||
for slug in APPLET_ORDER
|
||||
if slug in applets
|
||||
]
|
||||
|
||||
|
||||
def home_page(request):
|
||||
context = {
|
||||
"form": ItemForm(),
|
||||
@@ -53,7 +44,7 @@ def home_page(request):
|
||||
"page_class": "page-dashboard",
|
||||
}
|
||||
if request.user.is_authenticated:
|
||||
context["applets"] = _applet_context(request.user)
|
||||
context["applets"] = applet_context(request.user, "dashboard")
|
||||
context["recent_lists"] = _recent_lists(request.user)
|
||||
return render(request, "apps/dashboard/home.html", context)
|
||||
|
||||
@@ -73,7 +64,7 @@ def new_list(request):
|
||||
"page_class": "page-dashboard",
|
||||
}
|
||||
if request.user.is_authenticated:
|
||||
context["applets"] = _applet_context(request.user)
|
||||
context["applets"] = applet_context(request.user, "dashboard")
|
||||
context["recent_lists"] = _recent_lists(request.user)
|
||||
return render(request, "apps/dashboard/home.html", context)
|
||||
|
||||
@@ -135,7 +126,7 @@ def set_profile(request):
|
||||
@login_required(login_url="/")
|
||||
def toggle_applets(request):
|
||||
checked = request.POST.getlist("applets")
|
||||
for applet in Applet.objects.all():
|
||||
for applet in Applet.objects.filter(context="dashboard"):
|
||||
UserApplet.objects.update_or_create(
|
||||
user=request.user,
|
||||
applet=applet,
|
||||
@@ -143,7 +134,7 @@ def toggle_applets(request):
|
||||
)
|
||||
if request.headers.get("HX-Request"):
|
||||
return render(request, "apps/dashboard/_partials/_applets.html", {
|
||||
"applets": _applet_context(request.user),
|
||||
"applets": applet_context(request.user, "dashboard"),
|
||||
"palettes": PALETTES,
|
||||
"form": ItemForm(),
|
||||
"recent_lists": _recent_lists(request.user),
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import lxml.html
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from apps.applets.models import Applet, UserApplet
|
||||
from apps.lyric.models import User
|
||||
|
||||
|
||||
class GameboardViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="capman@test.io")
|
||||
self.user = User.objects.create(email="gamer@test.io")
|
||||
self.client.force_login(self.user)
|
||||
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
|
||||
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
|
||||
Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
|
||||
response = self.client.get("/gameboard/")
|
||||
self.parsed = lxml.html.fromstring(response.content)
|
||||
|
||||
@@ -29,11 +34,11 @@ class GameboardViewTest(TestCase):
|
||||
def test_gameboard_shows_new_game_applet(self):
|
||||
[_] = self.parsed.cssselect("#id_applet_new_game")
|
||||
|
||||
def test_gameboard_shows_game_kit_btn(self):
|
||||
[_] = self.parsed.cssselect("#id_game_kit_btn")
|
||||
def test_gameboard_shows_game_kit(self):
|
||||
[_] = self.parsed.cssselect("#id_game_kit")
|
||||
|
||||
def test_gameboard_shows_game_gear(self):
|
||||
[_] = self.parsed.cssselect("#id_game_gear")
|
||||
[_] = self.parsed.cssselect(".gear-btn")
|
||||
|
||||
def test_my_games_has_no_game_items_for_new_user(self):
|
||||
game_items = self.parsed.cssselect("#id_applet_my_games .game-item")
|
||||
@@ -50,3 +55,49 @@ class GameboardViewTest(TestCase):
|
||||
|
||||
def test_game_kit_has_dice_set_placeholder(self):
|
||||
[_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set")
|
||||
|
||||
|
||||
class ToggleGameAppletsViewTest(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create(email="gamer@test.io")
|
||||
self.client.force_login(self.user)
|
||||
self.new_game, _ = Applet.objects.get_or_create(
|
||||
slug="new-game", defaults={"name": "New Game", "context": "gameboard"}
|
||||
)
|
||||
self.my_games, _ = Applet.objects.get_or_create(
|
||||
slug="my-games", defaults={"name": "My Games", "context": "gameboard"}
|
||||
)
|
||||
self.url = reverse("toggle_game_applets")
|
||||
|
||||
def test_unauthenticated_user_is_redirected(self):
|
||||
self.client.logout()
|
||||
response = self.client.post(self.url)
|
||||
self.assertRedirects(
|
||||
response, f"/?next={self.url}", fetch_redirect_response=False
|
||||
)
|
||||
|
||||
def test_unchecked_applet_gets_user_applet_with_visible_false(self):
|
||||
self.client.post(self.url, {"applets": ["new-game"]})
|
||||
ua = UserApplet.objects.get(user=self.user, applet=self.my_games)
|
||||
self.assertFalse(ua.visible)
|
||||
|
||||
def test_redirects_on_normal_post(self):
|
||||
response = self.client.post(self.url, {"applets": ["new-game", "my-games"]})
|
||||
self.assertRedirects(
|
||||
response, reverse("gameboard"), fetch_redirect_response=False
|
||||
)
|
||||
|
||||
def test_returns_200_on_htmx_post(self):
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{"applets": ["new-game", "my-games"]},
|
||||
HTTP_HX_REQUEST="true",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_does_not_affect_dash_applets(self):
|
||||
dash_applet, _ = Applet.objects.get_or_create(
|
||||
slug="username", defaults={"name": "Username", "context": "dashboard"}
|
||||
)
|
||||
self.client.post(self.url, {"applets": ["new-game", "my-games"]})
|
||||
self.assertFalse(UserApplet.objects.filter(user=self.user, applet=dash_applet).exists())
|
||||
|
||||
@@ -5,5 +5,6 @@ from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.gameboard, name='gameboard'),
|
||||
path('toggle-applets', views.toggle_game_applets, name='toggle_game_applets'),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from apps.applets.utils import applet_context
|
||||
from apps.applets.models import Applet, UserApplet
|
||||
from apps.lyric.models import Token
|
||||
|
||||
|
||||
GAMEBOARD_APPLET_ORDER = [
|
||||
"new-game",
|
||||
"my-games",
|
||||
"game-kit",
|
||||
]
|
||||
|
||||
|
||||
@login_required(login_url="/")
|
||||
def gameboard(request):
|
||||
coin = request.user.tokens.filter(token_type=Token.COIN).first()
|
||||
@@ -12,5 +21,24 @@ def gameboard(request):
|
||||
request, "apps/gameboard/gameboard.html", {
|
||||
"coin": coin,
|
||||
"free_tokens": free_tokens,
|
||||
"applets": applet_context(request.user, "gameboard"),
|
||||
"page_class": "page-gameboard",
|
||||
}
|
||||
)
|
||||
|
||||
@login_required(login_url="/")
|
||||
def toggle_game_applets(request):
|
||||
checked = request.POST.getlist("applets")
|
||||
for applet in Applet.objects.filter(context="gameboard"):
|
||||
UserApplet.objects.update_or_create(
|
||||
user=request.user,
|
||||
applet=applet,
|
||||
defaults={"visible": applet.slug in checked},
|
||||
)
|
||||
if request.headers.get("HX-Request"):
|
||||
return render(request, "apps/gameboard/_partials/_applets.html", {
|
||||
"applets": applet_context(request.user, "gameboard"),
|
||||
"coin": request.user.tokens.filter(token_type=Token.COIN).first(),
|
||||
"free_tokens": list(request.user.tokens.filter(token_type=Token.FREE)),
|
||||
})
|
||||
return redirect("gameboard")
|
||||
|
||||
@@ -54,16 +54,16 @@ class DashboardMaintenanceTest(FunctionalTest):
|
||||
self.browser.find_element(By.ID, "id_applet_username")
|
||||
self.browser.find_element(By.ID, "id_applet_palette")
|
||||
# 3. Click el w. id="id_dash_gear"
|
||||
dash_gear = self.browser.find_element(By.ID, "id_dash_gear")
|
||||
dash_gear = self.browser.find_element(By.CSS_SELECTOR, ".gear-btn")
|
||||
dash_gear.click()
|
||||
# 4. A menu appears; wait_for el w. id="id_applet_menu"
|
||||
# 4. A menu appears; wait_for el w. id="id_dash_applet_menu"
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
# 5. Find two checkboxes in menu, name="username" & name="palette"; assert both .is_selected()
|
||||
menu = self.browser.find_element(By.ID, "id_applet_menu")
|
||||
menu = self.browser.find_element(By.ID, "id_dash_applet_menu")
|
||||
username_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="username"]')
|
||||
palette_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="palette"]')
|
||||
self.assertTrue(username_cb.is_selected())
|
||||
@@ -76,7 +76,7 @@ class DashboardMaintenanceTest(FunctionalTest):
|
||||
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
# 8. wait_for palette applet to be gone
|
||||
@@ -93,10 +93,10 @@ class DashboardMaintenanceTest(FunctionalTest):
|
||||
dash_gear.click()
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
menu = self.browser.find_element(By.ID, "id_applet_menu")
|
||||
menu = self.browser.find_element(By.ID, "id_dash_applet_menu")
|
||||
palette_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="palette"]')
|
||||
self.assertFalse(palette_cb.is_selected())
|
||||
# 11. Click it to re-check box; submit
|
||||
@@ -106,7 +106,7 @@ class DashboardMaintenanceTest(FunctionalTest):
|
||||
# 12. wait_for id_applet_palette to reappear
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
self.wait_for(
|
||||
@@ -125,19 +125,19 @@ class AppletMenuDismissTest(FunctionalTest):
|
||||
self.browser.get(self.live_server_url)
|
||||
|
||||
def _open_menu(self):
|
||||
self.browser.find_element(By.ID, "id_dash_gear").click()
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
|
||||
def test_gear_click_toggles_menu_closed(self):
|
||||
self._open_menu()
|
||||
self.browser.find_element(By.ID, "id_dash_gear").click()
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -146,7 +146,7 @@ class AppletMenuDismissTest(FunctionalTest):
|
||||
self.browser.find_element(By.ID, "id_applet_menu_cancel").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -155,6 +155,6 @@ class AppletMenuDismissTest(FunctionalTest):
|
||||
self.browser.find_element(By.TAG_NAME, "h2").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_applet_menu").is_displayed()
|
||||
self.browser.find_element(By.ID, "id_dash_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from .base import FunctionalTest
|
||||
from apps.applets.models import Applet
|
||||
|
||||
|
||||
class GameboardNavigationTest(FunctionalTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
|
||||
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
|
||||
Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
|
||||
|
||||
def test_footer_links_to_gameboard(self):
|
||||
# 1. Log in, nav to dashboard
|
||||
self.create_pre_authenticated_session("capman@test.io")
|
||||
@@ -41,20 +49,12 @@ class GameboardNavigationTest(FunctionalTest):
|
||||
# 1. Log in, nav to gameboard
|
||||
self.create_pre_authenticated_session("capman@test.io")
|
||||
self.browser.get(self.live_server_url + "/gameboard/")
|
||||
# 2. Assert game kit & gear btns both present (stacked vertically)
|
||||
# 2. Assert game kit applet & gear btn present
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_game_kit_btn")
|
||||
lambda: self.browser.find_element(By.ID, "id_game_kit")
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_game_gear")
|
||||
# 3. Click game kit btn to open panel
|
||||
self.browser.find_element(By.ID, "id_game_kit_btn").click()
|
||||
# 4. Wait for game kit panel to become visible
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_game_kit").is_displayed()
|
||||
)
|
||||
)
|
||||
# 5. Assert Coin-on-a-String present in kit
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn")
|
||||
# 3. Assert Coin-on-a-String present in kit
|
||||
coin = self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
|
||||
# 6. Hover over it; assert tooltip shows name, entry text & reuse description
|
||||
ActionChains(self.browser).move_to_element(coin).perform()
|
||||
@@ -91,3 +91,75 @@ class GameboardNavigationTest(FunctionalTest):
|
||||
# 9. Assert card deck & dice set placeholder present
|
||||
self.browser.find_element(By.ID, "id_kit_card_deck")
|
||||
self.browser.find_element(By.ID, "id_kit_dice_set")
|
||||
|
||||
|
||||
class GameboardAppletMenuTest(FunctionalTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
Applet.objects.get_or_create(slug="new-game", defaults={"name": "New Game", "context": "gameboard"})
|
||||
Applet.objects.get_or_create(slug="my-games", defaults={"name": "My Games", "context": "gameboard"})
|
||||
self.create_pre_authenticated_session("gamer@test.io")
|
||||
self.browser.get(self.live_server_url + "/gameboard/")
|
||||
|
||||
def test_user_can_toggle_applet_visibility_via_gear_menu(self):
|
||||
# 1. Assert both applets present
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.ID, "id_applet_my_games")
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_applet_new_game")
|
||||
# 2. Click gear; wait for menu
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
# 3. Find checkboxes; assert both checked
|
||||
menu = self.browser.find_element(By.ID, "id_game_applet_menu")
|
||||
my_games_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="my-games"]')
|
||||
new_game_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="new-game"]')
|
||||
self.assertTrue(my_games_cb.is_selected())
|
||||
self.assertTrue(new_game_cb.is_selected())
|
||||
# 4. Uncheck my-games; plant no-reload marker; submit
|
||||
my_games_cb.click()
|
||||
self.assertFalse(my_games_cb.is_selected())
|
||||
self.browser.execute_script("window.__no_reload_marker = true")
|
||||
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
|
||||
# 5. Wait for menu to close; assert my-games gone, new game remains
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
self.wait_for(
|
||||
lambda: self.assertRaises(
|
||||
NoSuchElementException,
|
||||
self.browser.find_element,
|
||||
By.ID, "id_applet_my_games",
|
||||
)
|
||||
)
|
||||
self.browser.find_element(By.ID, "id_applet_new_game")
|
||||
# 6. Re-check my-games; assert it reappears
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn").click()
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
menu = self.browser.find_element(By.ID, "id_game_applet_menu")
|
||||
my_games_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="my-games"]')
|
||||
self.assertFalse(my_games_cb.is_selected())
|
||||
my_games_cb.click()
|
||||
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
|
||||
self.wait_for(
|
||||
lambda: self.assertFalse(
|
||||
self.browser.find_element(By.ID, "id_game_applet_menu").is_displayed()
|
||||
)
|
||||
)
|
||||
self.wait_for(
|
||||
lambda: self.assertTrue(
|
||||
self.browser.find_element(By.ID, "id_applet_my_games")
|
||||
)
|
||||
)
|
||||
# 7. Assert no full page reload occurred
|
||||
self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true"))
|
||||
|
||||
115
src/static_src/scss/_applets.scss
Normal file
115
src/static_src/scss/_applets.scss
Normal file
@@ -0,0 +1,115 @@
|
||||
// ── Gear button ────────────────────────────────────────────
|
||||
.gear-btn {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 1;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: rgba(var(--secUser), 1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(var(--priUser), 1);
|
||||
border: 0.15rem solid rgba(var(--secUser), 1);
|
||||
}
|
||||
|
||||
// ── Applet menu (shared structure) ─────────────────────────
|
||||
%applet-menu {
|
||||
position: absolute;
|
||||
bottom: 3rem;
|
||||
right: 0.5rem;
|
||||
z-index: 100;
|
||||
background-color: rgba(var(--priUser), 0.95);
|
||||
border: 0.15rem solid rgba(var(--secUser), 0.5);
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
|
||||
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||
;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
|
||||
.menu-btns {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||
border-radius: 0.25rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
top: 0.1em;
|
||||
|
||||
&:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.2em;
|
||||
bottom: 0.2em;
|
||||
width: 0.55em;
|
||||
height: 1em;
|
||||
border: 0.12em solid rgba(var(--ninUser), 1);
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#id_dash_applet_menu { @extend %applet-menu; }
|
||||
#id_game_applet_menu { @extend %applet-menu; }
|
||||
|
||||
// ── Applets grid (shared across all boards) ────────────────
|
||||
%applets-grid {
|
||||
container-type: inline-size;
|
||||
--grid-gap: 0.5rem;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-rows: 3rem;
|
||||
gap: var(--grid-gap);
|
||||
padding: 0.75rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 2%,
|
||||
black 98%,
|
||||
transparent 100%
|
||||
);
|
||||
|
||||
section {
|
||||
border: 0.2rem solid rgba(var(--secUser), 0.5);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
grid-column: span var(--applet-cols, 12);
|
||||
grid-row: span var(--applet-rows, 3);
|
||||
|
||||
@container (max-width: 550px) {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#id_applets_container { @extend %applets-grid; }
|
||||
#id_game_applets_container { @extend %applets-grid; }
|
||||
@@ -28,110 +28,7 @@ body.page-dashboard {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#id_dash_gear {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 2rem;
|
||||
cursor: pointer;
|
||||
color: rgba(var(--secUser), 1);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(var(--priUser), 1);
|
||||
border: 0.15rem solid rgba(var(--secUser), 1);
|
||||
}
|
||||
|
||||
#id_applet_menu {
|
||||
position: absolute;
|
||||
bottom: 3rem;
|
||||
right: 0.5rem;
|
||||
z-index: 100;
|
||||
background-color: rgba(var(--priUser), 0.95);
|
||||
border: 0.15rem solid rgba(var(--secUser), 0.5);
|
||||
box-shadow:
|
||||
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
|
||||
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
|
||||
;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
|
||||
.menu-btns {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||
border-radius: 0.25rem;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
top: 0.1em;
|
||||
|
||||
&:checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0.2em;
|
||||
bottom: 0.2em;
|
||||
width: 0.55em;
|
||||
height: 1em;
|
||||
border: 0.12em solid rgba(var(--ninUser), 1);
|
||||
border-top: none;
|
||||
border-left: none;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#id_applets_container {
|
||||
container-type: inline-size;
|
||||
--grid-gap: 0.5rem;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
grid-auto-rows: 3rem;
|
||||
gap: var(--grid-gap);
|
||||
padding: 0.75rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0%,
|
||||
black 2%,
|
||||
black 98%,
|
||||
transparent 100%
|
||||
);
|
||||
|
||||
section {
|
||||
border: 0.2rem solid rgba(var(--secUser), 0.5);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
grid-column: span var(--applet-cols, 12);
|
||||
grid-row: span var(--applet-rows, 3);
|
||||
}
|
||||
|
||||
#id_applet_my_lists {
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
@@ -240,12 +137,6 @@ body.page-dashboard {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#id_applets_container {
|
||||
section {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 738px) {
|
||||
|
||||
73
src/static_src/scss/_gameboard.scss
Normal file
73
src/static_src/scss/_gameboard.scss
Normal file
@@ -0,0 +1,73 @@
|
||||
html:has(body.page-gameboard) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body.page-gameboard {
|
||||
overflow: hidden;
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: -1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.gameboard-page {
|
||||
flex: 1;
|
||||
min-width: 425px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.gameboard-page {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 738px) {
|
||||
.gameboard-page {
|
||||
min-width: 666px;
|
||||
}
|
||||
}
|
||||
|
||||
#id_game_applets_container {
|
||||
#id_applet_game_kit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
#id_game_kit {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 500px) {
|
||||
body.page-gameboard {
|
||||
.container {
|
||||
.row {
|
||||
padding: 0.25rem 0;
|
||||
.col-lg-6 h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
@import 'rootvars';
|
||||
@import 'applets';
|
||||
@import 'base';
|
||||
@import 'button-pad';
|
||||
@import 'dashboard';
|
||||
@import 'gameboard';
|
||||
@import 'palette-picker';
|
||||
@import 'wallet-tokens';
|
||||
|
||||
|
||||
7
src/templates/apps/applets/_partials/_applets.html
Normal file
7
src/templates/apps/applets/_partials/_applets.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% for entry in applets %}
|
||||
{% if entry.visible %}
|
||||
{% with "apps/"|add:entry.applet.context|add:"/_partials/_applet-"|add:entry.applet.slug|add:".html" as partial %}
|
||||
{% include partial %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
3
src/templates/apps/applets/_partials/_gear.html
Normal file
3
src/templates/apps/applets/_partials/_gear.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<button class="gear-btn" data-menu-target="{{ menu_id }}">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</button>
|
||||
17
src/templates/apps/dashboard/_partials/_applet-my-lists.html
Normal file
17
src/templates/apps/dashboard/_partials/_applet-my-lists.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<section
|
||||
id="id_applet_my_lists"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<a href="{% url 'my_lists' user.id %}" class="my-lists-main">My lists:</a>
|
||||
<div class="my-lists-container">
|
||||
<ul>
|
||||
{% for list in recent_lists %}
|
||||
<li>
|
||||
<a href="{{ list.get_absolute_url }}">{{ list.name }}</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>No lists yet.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,8 @@
|
||||
<section
|
||||
id="id_applet_new_list"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2>Start a new to-do list</h2>
|
||||
{% url "new_list" as form_action %}
|
||||
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
|
||||
</section>
|
||||
21
src/templates/apps/dashboard/_partials/_applet-palette.html
Normal file
21
src/templates/apps/dashboard/_partials/_applet-palette.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<section
|
||||
id="id_applet_palette"
|
||||
class="palette"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<div class="palette-scroll">
|
||||
{% for palette in palettes %}
|
||||
<div class="palette-item">
|
||||
<div class="swatch {{ palette.name }}{% if user_palette == palette.name %} active{% endif %}{% if palette.locked %} locked{% endif %}"></div>
|
||||
{% if not palette.locked %}
|
||||
<form method="POST" action="{% url "set_palette" %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="palette" value="{{ palette.name }}" class="btn btn-confirm">OK</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="btn btn-disabled">×</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
23
src/templates/apps/dashboard/_partials/_applet-username.html
Normal file
23
src/templates/apps/dashboard/_partials/_applet-username.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<section
|
||||
id="id_applet_username"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<form method="POST" action="{% url "set_profile" %}">
|
||||
{% csrf_token %}
|
||||
<div class="username-field">
|
||||
<span class="username-at">@</span>
|
||||
<input
|
||||
id="id_new_username"
|
||||
name="username"
|
||||
required
|
||||
value="{{ user.username|default:'' }}"
|
||||
autocomplete="off"
|
||||
placeholder="username"
|
||||
data-original="{{ user.username|default:'' }}"
|
||||
oninput="this.closest('form').querySelector('.save-btn').hidden = (this.value === this.dataset.original)"
|
||||
onblur="this.scrollLeft = 0"
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-confirm save-btn" hidden>OK</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -0,0 +1,7 @@
|
||||
<section
|
||||
id="id_applet_wallet"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<span>Writs: {{ user.wallet.writs }}</span>
|
||||
<a href="{% url "wallet" %}" class="wallet-manage-link">Manage Wallet</a>
|
||||
</section>
|
||||
@@ -1,10 +1,11 @@
|
||||
{% load lyric_extras %}
|
||||
|
||||
<div id="id_applets_container">
|
||||
<div id="id_applet_menu" style="display:none;">
|
||||
<div id="id_dash_applet_menu" style="display:none;">
|
||||
<form
|
||||
hx-post="{% url "toggle_applets" %}"
|
||||
hx-target="#id_applets_container"
|
||||
hx-swap="outerHTML"
|
||||
hx-post="{% url "toggle_applets" %}"
|
||||
hx-target="#id_applets_container"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
{% csrf_token %}
|
||||
{% for entry in applets %}
|
||||
@@ -20,95 +21,9 @@
|
||||
{% endfor %}
|
||||
<div class="menu-btns">
|
||||
<button type="submit" class="btn btn-confirm">OK</button>
|
||||
<button type="button" id="id_applet_menu_cancel" class="btn btn-cancel">NVM</button>
|
||||
<button type="button" id="id_applet_menu_cancel" class="btn btn-cancel applet-menu-cancel">NVM</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% for entry in applets %}
|
||||
{% if entry.visible %}
|
||||
{% if entry.applet.slug == "wallet" %}
|
||||
<section
|
||||
id="id_applet_wallet"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<span>Writs: {{ user.wallet.writs }}</span>
|
||||
<a href="{% url "wallet" %}" class="wallet-manage-link">Manage Wallet</a>
|
||||
</section>
|
||||
{% elif entry.applet.slug == "new-list" %}
|
||||
<section
|
||||
id="id_applet_new_list"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2>Start a new to-do list</h2>
|
||||
{% url "new_list" as form_action %}
|
||||
{% include "apps/dashboard/_partials/_form.html" with form=form form_action=form_action %}
|
||||
</section>
|
||||
{% elif entry.applet.slug == "my-lists" %}
|
||||
<section
|
||||
id="id_applet_my_lists"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<a href="{% url 'my_lists' user.id %}" class="my-lists-main">My lists:</a>
|
||||
<div class="my-lists-container">
|
||||
<ul>
|
||||
{% for list in recent_lists %}
|
||||
<li>
|
||||
<a href="{{ list.get_absolute_url }}">{{ list.name }}</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>No lists yet.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
{% elif entry.applet.slug == "username" %}
|
||||
<section
|
||||
id="id_applet_username"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<form method="POST" action="{% url "set_profile" %}">
|
||||
{% csrf_token %}
|
||||
<div class="username-field">
|
||||
<span class="username-at">@</span>
|
||||
<input
|
||||
id="id_new_username"
|
||||
name="username"
|
||||
required
|
||||
value="{{ user.username|default:'' }}"
|
||||
autocomplete="off"
|
||||
placeholder="username"
|
||||
data-original="{{ user.username|default:'' }}"
|
||||
oninput="this.closest('form').querySelector('.save-btn').hidden = (this.value === this.dataset.original)"
|
||||
onblur="this.scrollLeft = 0"
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-confirm save-btn" hidden>OK</button>
|
||||
</form>
|
||||
</section>
|
||||
{% elif entry.applet.slug == "palette" %}
|
||||
<section
|
||||
id="id_applet_palette"
|
||||
class="palette"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<div class="palette-scroll">
|
||||
{% for palette in palettes %}
|
||||
<div class="palette-item">
|
||||
<div class="swatch {{ palette.name }}{% if user_palette == palette.name %} active{% endif %}{% if palette.locked %} locked{% endif %}"></div>
|
||||
{% if not palette.locked %}
|
||||
<form method="POST" action="{% url "set_palette" %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" name="palette" value="{{ palette.name }}" class="btn btn-confirm">OK</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="btn btn-disabled">×</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% include "apps/applets/_partials/_applets.html" %}
|
||||
</div>
|
||||
@@ -2,6 +2,5 @@
|
||||
<script>
|
||||
window.onload = () => {
|
||||
initialize("#id_text");
|
||||
initGearMenu();
|
||||
};
|
||||
</script>
|
||||
@@ -11,9 +11,7 @@
|
||||
{% block content %}
|
||||
{% if user.is_authenticated %}
|
||||
<div id="id_dash_content">
|
||||
<button id="id_dash_gear">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
</button>
|
||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_dash_applet_menu" %}
|
||||
{% include "apps/dashboard/_partials/_applets.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
22
src/templates/apps/gameboard/_partials/_applet-game-kit.html
Normal file
22
src/templates/apps/gameboard/_partials/_applet-game-kit.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<section
|
||||
id="id_applet_game_kit"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2>Game Kit</h2>
|
||||
<div id="id_game_kit">
|
||||
{% if coin %}
|
||||
<div id="id_kit_coin_on_a_string" class="token">
|
||||
<i class="fa-solid fa-clover"></i>
|
||||
<span class="token-tooltip">{{ coin.tooltip_text }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for token in free_tokens %}
|
||||
<div id="id_kit_free_token_{{ forloop.counter0 }}" class="token">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
<span class="token-tooltip">{{ token.tooltip_text }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="id_kit_card_deck" class="kit-item"><i class="fa-regular fa-id-badge"></i></div>
|
||||
<div id="id_kit_dice_set" class="kit-item"><i class="fa-solid fa-dice"></i></div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,9 @@
|
||||
<section
|
||||
id="id_applet_my_games"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2>My Games</h2>
|
||||
<ul class="game-list">
|
||||
<small>[feature forthcoming]</small>
|
||||
</ul>
|
||||
</section>
|
||||
@@ -0,0 +1,9 @@
|
||||
<section
|
||||
id="id_applet_new_game"
|
||||
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
|
||||
>
|
||||
<h2>New Game</h2>
|
||||
<ul class="game-type">
|
||||
<small>[feature forthcoming]</small>
|
||||
</ul>
|
||||
</section>
|
||||
27
src/templates/apps/gameboard/_partials/_applets.html
Normal file
27
src/templates/apps/gameboard/_partials/_applets.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<div id="id_game_applets_container">
|
||||
<div id="id_game_applet_menu" style="display:none;">
|
||||
<form
|
||||
hx-post="{% url "toggle_game_applets" %}"
|
||||
hx-target="#id_game_applets_container"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
{% csrf_token %}
|
||||
{% for entry in applets %}
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="applets"
|
||||
value="{{ entry.applet.slug }}"
|
||||
{% if entry.visible %}checked{% endif %}
|
||||
>
|
||||
{{ entry.applet.name }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<div class="menu-btns">
|
||||
<button type="submit" class="btn btn-confirm">OK</button>
|
||||
<button type="button" id="id_game_applet_menu_cancel" class="btn btn-cancel applet-menu-cancel">NVM</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% include "apps/applets/_partials/_applets.html" %}
|
||||
</div>
|
||||
@@ -5,36 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="gameboard-page">
|
||||
<section id="id_applet_my_games">
|
||||
<h2>My Games</h2>
|
||||
</section>
|
||||
|
||||
<section id="id_applet_new_game">
|
||||
<h2>New Game</h2>
|
||||
</section>
|
||||
|
||||
<div id="id_game_gear"></div>
|
||||
<button
|
||||
id="id_game_kit_btn"
|
||||
onclick="document.getElementById('id_game_kit').style.display='block'"
|
||||
>
|
||||
Game Kit
|
||||
</button>
|
||||
<div id="id_game_kit" style="display:none;">
|
||||
{% if coin %}
|
||||
<div id="id_kit_coin_on_a_string" class="token">
|
||||
<i class="fa-solid fa-clover"></i>
|
||||
<span class="token-tooltip">{{ coin.tooltip_text }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for token in free_tokens %}
|
||||
<div id="id_kit_free_token_{{ forloop.counter0 }}" class="token">
|
||||
<i class="fa-solid fa-coins"></i>
|
||||
<span class="token-tooltip">{{ token.tooltip_text }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div id="id_kit_card_deck" class="kit-item"><i class="fa-regular fa-id-badge"></i></div>
|
||||
<div id="id_kit_dice_set" class="kit-item"><i class="fa-solid fa-dice"></i></div>
|
||||
</div>
|
||||
{% include "apps/applets/_partials/_gear.html" with menu_id="id_game_applet_menu" %}
|
||||
{% include "apps/gameboard/_partials/_applets.html" %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
@@ -53,6 +53,7 @@
|
||||
{% block scripts %}
|
||||
{% endblock scripts %}
|
||||
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
||||
<script src="{% static "apps/scripts/applets.js" %}"></script>
|
||||
<script>
|
||||
document.body.addEventListener('htmx:configRequest', function(evt) {
|
||||
evt.detail.headers['X-CSRFToken'] = getCookie('csrftoken');
|
||||
|
||||
Reference in New Issue
Block a user