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 django.test import TestCase
|
||||||
|
|
||||||
from apps.applets.models import Applet, UserApplet
|
from apps.applets.models import Applet, UserApplet
|
||||||
|
from apps.applets.utils import applet_context
|
||||||
from apps.lyric.models import User
|
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_cols, 12)
|
||||||
self.assertEqual(self.applet.grid_rows, 3)
|
self.assertEqual(self.applet.grid_rows, 3)
|
||||||
|
|
||||||
|
|
||||||
class UserAppletModelTest(TestCase):
|
class UserAppletModelTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create(email="a@b.cde")
|
self.user = User.objects.create(email="a@b.cde")
|
||||||
@@ -38,3 +40,26 @@ class UserAppletModelTest(TestCase):
|
|||||||
UserApplet.objects.create(user=self.user, applet=self.applet, visible=True)
|
UserApplet.objects.create(user=self.user, applet=self.applet, visible=True)
|
||||||
with self.assertRaises(IntegrityError):
|
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");
|
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()
|
response = self.post_invalid_input()
|
||||||
self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))
|
self.assertContains(response, html.escape(EMPTY_ITEM_ERROR))
|
||||||
|
|
||||||
|
@override_settings(COMPRESS_ENABLED=False)
|
||||||
class ListViewTest(TestCase):
|
class ListViewTest(TestCase):
|
||||||
def test_uses_list_template(self):
|
def test_uses_list_template(self):
|
||||||
mylist = List.objects.create()
|
mylist = List.objects.create()
|
||||||
@@ -358,7 +359,7 @@ class ProfileViewTest(TestCase):
|
|||||||
[username_input] = parsed.cssselect("#id_new_username")
|
[username_input] = parsed.cssselect("#id_new_username")
|
||||||
self.assertEqual("discoman", username_input.get("value"))
|
self.assertEqual("discoman", username_input.get("value"))
|
||||||
|
|
||||||
class ToggleAppletsViewTest(TestCase):
|
class ToggleDashAppletsViewTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create(email="disco@test.io")
|
self.user = User.objects.create(email="disco@test.io")
|
||||||
self.client.force_login(self.user)
|
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_username")), 1)
|
||||||
self.assertEqual(len(parsed.cssselect("#id_applet_palette")), 0)
|
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):
|
class AppletVisibilityContextTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create(email="disco@test.io")
|
self.user = User.objects.create(email="disco@test.io")
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import lxml.html
|
import lxml.html
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import override_settings, TestCase
|
||||||
|
|
||||||
from apps.lyric.models import Token, User, Wallet
|
from apps.lyric.models import Token, User, Wallet
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(COMPRESS_ENABLED=False)
|
||||||
class WalletViewTest(TestCase):
|
class WalletViewTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = User.objects.create(email="capman@test.io")
|
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 django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
|
||||||
from apps.applets.models import Applet, UserApplet
|
from apps.applets.models import Applet, UserApplet
|
||||||
|
from apps.applets.utils import applet_context
|
||||||
from apps.dashboard.forms import ExistingListItemForm, ItemForm
|
from apps.dashboard.forms import ExistingListItemForm, ItemForm
|
||||||
from apps.dashboard.models import Item, List
|
from apps.dashboard.models import Item, List
|
||||||
from apps.lyric.models import PaymentMethod, Token, User, Wallet
|
from apps.lyric.models import PaymentMethod, Token, User, Wallet
|
||||||
@@ -36,16 +37,6 @@ def _recent_lists(user, limit=3):
|
|||||||
.distinct()[:limit]
|
.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):
|
def home_page(request):
|
||||||
context = {
|
context = {
|
||||||
"form": ItemForm(),
|
"form": ItemForm(),
|
||||||
@@ -53,7 +44,7 @@ def home_page(request):
|
|||||||
"page_class": "page-dashboard",
|
"page_class": "page-dashboard",
|
||||||
}
|
}
|
||||||
if request.user.is_authenticated:
|
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)
|
context["recent_lists"] = _recent_lists(request.user)
|
||||||
return render(request, "apps/dashboard/home.html", context)
|
return render(request, "apps/dashboard/home.html", context)
|
||||||
|
|
||||||
@@ -73,7 +64,7 @@ def new_list(request):
|
|||||||
"page_class": "page-dashboard",
|
"page_class": "page-dashboard",
|
||||||
}
|
}
|
||||||
if request.user.is_authenticated:
|
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)
|
context["recent_lists"] = _recent_lists(request.user)
|
||||||
return render(request, "apps/dashboard/home.html", context)
|
return render(request, "apps/dashboard/home.html", context)
|
||||||
|
|
||||||
@@ -135,7 +126,7 @@ def set_profile(request):
|
|||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def toggle_applets(request):
|
def toggle_applets(request):
|
||||||
checked = request.POST.getlist("applets")
|
checked = request.POST.getlist("applets")
|
||||||
for applet in Applet.objects.all():
|
for applet in Applet.objects.filter(context="dashboard"):
|
||||||
UserApplet.objects.update_or_create(
|
UserApplet.objects.update_or_create(
|
||||||
user=request.user,
|
user=request.user,
|
||||||
applet=applet,
|
applet=applet,
|
||||||
@@ -143,7 +134,7 @@ def toggle_applets(request):
|
|||||||
)
|
)
|
||||||
if request.headers.get("HX-Request"):
|
if request.headers.get("HX-Request"):
|
||||||
return render(request, "apps/dashboard/_partials/_applets.html", {
|
return render(request, "apps/dashboard/_partials/_applets.html", {
|
||||||
"applets": _applet_context(request.user),
|
"applets": applet_context(request.user, "dashboard"),
|
||||||
"palettes": PALETTES,
|
"palettes": PALETTES,
|
||||||
"form": ItemForm(),
|
"form": ItemForm(),
|
||||||
"recent_lists": _recent_lists(request.user),
|
"recent_lists": _recent_lists(request.user),
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import lxml.html
|
import lxml.html
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from apps.applets.models import Applet, UserApplet
|
||||||
from apps.lyric.models import User
|
from apps.lyric.models import User
|
||||||
|
|
||||||
|
|
||||||
class GameboardViewTest(TestCase):
|
class GameboardViewTest(TestCase):
|
||||||
def setUp(self):
|
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)
|
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/")
|
response = self.client.get("/gameboard/")
|
||||||
self.parsed = lxml.html.fromstring(response.content)
|
self.parsed = lxml.html.fromstring(response.content)
|
||||||
|
|
||||||
@@ -29,11 +34,11 @@ class GameboardViewTest(TestCase):
|
|||||||
def test_gameboard_shows_new_game_applet(self):
|
def test_gameboard_shows_new_game_applet(self):
|
||||||
[_] = self.parsed.cssselect("#id_applet_new_game")
|
[_] = self.parsed.cssselect("#id_applet_new_game")
|
||||||
|
|
||||||
def test_gameboard_shows_game_kit_btn(self):
|
def test_gameboard_shows_game_kit(self):
|
||||||
[_] = self.parsed.cssselect("#id_game_kit_btn")
|
[_] = self.parsed.cssselect("#id_game_kit")
|
||||||
|
|
||||||
def test_gameboard_shows_game_gear(self):
|
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):
|
def test_my_games_has_no_game_items_for_new_user(self):
|
||||||
game_items = self.parsed.cssselect("#id_applet_my_games .game-item")
|
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):
|
def test_game_kit_has_dice_set_placeholder(self):
|
||||||
[_] = self.parsed.cssselect("#id_game_kit #id_kit_dice_set")
|
[_] = 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 = [
|
urlpatterns = [
|
||||||
path('', views.gameboard, name='gameboard'),
|
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.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
|
from apps.lyric.models import Token
|
||||||
|
|
||||||
|
|
||||||
|
GAMEBOARD_APPLET_ORDER = [
|
||||||
|
"new-game",
|
||||||
|
"my-games",
|
||||||
|
"game-kit",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@login_required(login_url="/")
|
@login_required(login_url="/")
|
||||||
def gameboard(request):
|
def gameboard(request):
|
||||||
coin = request.user.tokens.filter(token_type=Token.COIN).first()
|
coin = request.user.tokens.filter(token_type=Token.COIN).first()
|
||||||
@@ -12,5 +21,24 @@ def gameboard(request):
|
|||||||
request, "apps/gameboard/gameboard.html", {
|
request, "apps/gameboard/gameboard.html", {
|
||||||
"coin": coin,
|
"coin": coin,
|
||||||
"free_tokens": free_tokens,
|
"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_username")
|
||||||
self.browser.find_element(By.ID, "id_applet_palette")
|
self.browser.find_element(By.ID, "id_applet_palette")
|
||||||
# 3. Click el w. id="id_dash_gear"
|
# 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()
|
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(
|
self.wait_for(
|
||||||
lambda: self.assertTrue(
|
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()
|
# 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"]')
|
username_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="username"]')
|
||||||
palette_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="palette"]')
|
palette_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="palette"]')
|
||||||
self.assertTrue(username_cb.is_selected())
|
self.assertTrue(username_cb.is_selected())
|
||||||
@@ -76,7 +76,7 @@ class DashboardMaintenanceTest(FunctionalTest):
|
|||||||
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
|
menu.find_element(By.CSS_SELECTOR, '[type="submit"]').click()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
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
|
# 8. wait_for palette applet to be gone
|
||||||
@@ -93,10 +93,10 @@ class DashboardMaintenanceTest(FunctionalTest):
|
|||||||
dash_gear.click()
|
dash_gear.click()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertTrue(
|
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"]')
|
palette_cb = menu.find_element(By.CSS_SELECTOR, '[name="applets"][value="palette"]')
|
||||||
self.assertFalse(palette_cb.is_selected())
|
self.assertFalse(palette_cb.is_selected())
|
||||||
# 11. Click it to re-check box; submit
|
# 11. Click it to re-check box; submit
|
||||||
@@ -106,7 +106,7 @@ class DashboardMaintenanceTest(FunctionalTest):
|
|||||||
# 12. wait_for id_applet_palette to reappear
|
# 12. wait_for id_applet_palette to reappear
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
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(
|
self.wait_for(
|
||||||
@@ -125,19 +125,19 @@ class AppletMenuDismissTest(FunctionalTest):
|
|||||||
self.browser.get(self.live_server_url)
|
self.browser.get(self.live_server_url)
|
||||||
|
|
||||||
def _open_menu(self):
|
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(
|
self.wait_for(
|
||||||
lambda: self.assertTrue(
|
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):
|
def test_gear_click_toggles_menu_closed(self):
|
||||||
self._open_menu()
|
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(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
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.browser.find_element(By.ID, "id_applet_menu_cancel").click()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
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.browser.find_element(By.TAG_NAME, "h2").click()
|
||||||
self.wait_for(
|
self.wait_for(
|
||||||
lambda: self.assertFalse(
|
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.action_chains import ActionChains
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
|
||||||
from .base import FunctionalTest
|
from .base import FunctionalTest
|
||||||
|
from apps.applets.models import Applet
|
||||||
|
|
||||||
|
|
||||||
class GameboardNavigationTest(FunctionalTest):
|
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):
|
def test_footer_links_to_gameboard(self):
|
||||||
# 1. Log in, nav to dashboard
|
# 1. Log in, nav to dashboard
|
||||||
self.create_pre_authenticated_session("capman@test.io")
|
self.create_pre_authenticated_session("capman@test.io")
|
||||||
@@ -41,20 +49,12 @@ class GameboardNavigationTest(FunctionalTest):
|
|||||||
# 1. Log in, nav to gameboard
|
# 1. Log in, nav to gameboard
|
||||||
self.create_pre_authenticated_session("capman@test.io")
|
self.create_pre_authenticated_session("capman@test.io")
|
||||||
self.browser.get(self.live_server_url + "/gameboard/")
|
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(
|
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")
|
self.browser.find_element(By.CSS_SELECTOR, ".gear-btn")
|
||||||
# 3. Click game kit btn to open panel
|
# 3. Assert Coin-on-a-String present in kit
|
||||||
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
|
|
||||||
coin = self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
|
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
|
# 6. Hover over it; assert tooltip shows name, entry text & reuse description
|
||||||
ActionChains(self.browser).move_to_element(coin).perform()
|
ActionChains(self.browser).move_to_element(coin).perform()
|
||||||
@@ -91,3 +91,75 @@ class GameboardNavigationTest(FunctionalTest):
|
|||||||
# 9. Assert card deck & dice set placeholder present
|
# 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_card_deck")
|
||||||
self.browser.find_element(By.ID, "id_kit_dice_set")
|
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;
|
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 {
|
#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 {
|
#id_applet_my_lists {
|
||||||
padding: 1.25rem 1.5rem;
|
padding: 1.25rem 1.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -240,12 +137,6 @@ body.page-dashboard {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#id_applets_container {
|
|
||||||
section {
|
|
||||||
grid-column: span 12;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 738px) {
|
@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 'rootvars';
|
||||||
|
@import 'applets';
|
||||||
@import 'base';
|
@import 'base';
|
||||||
@import 'button-pad';
|
@import 'button-pad';
|
||||||
@import 'dashboard';
|
@import 'dashboard';
|
||||||
|
@import 'gameboard';
|
||||||
@import 'palette-picker';
|
@import 'palette-picker';
|
||||||
@import 'wallet-tokens';
|
@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,6 +1,7 @@
|
|||||||
{% load lyric_extras %}
|
{% load lyric_extras %}
|
||||||
|
|
||||||
<div id="id_applets_container">
|
<div id="id_applets_container">
|
||||||
<div id="id_applet_menu" style="display:none;">
|
<div id="id_dash_applet_menu" style="display:none;">
|
||||||
<form
|
<form
|
||||||
hx-post="{% url "toggle_applets" %}"
|
hx-post="{% url "toggle_applets" %}"
|
||||||
hx-target="#id_applets_container"
|
hx-target="#id_applets_container"
|
||||||
@@ -20,95 +21,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="menu-btns">
|
<div class="menu-btns">
|
||||||
<button type="submit" class="btn btn-confirm">OK</button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{% include "apps/applets/_partials/_applets.html" %}
|
||||||
{% 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 %}
|
|
||||||
</div>
|
</div>
|
||||||
@@ -2,6 +2,5 @@
|
|||||||
<script>
|
<script>
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
initialize("#id_text");
|
initialize("#id_text");
|
||||||
initGearMenu();
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -11,9 +11,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<div id="id_dash_content">
|
<div id="id_dash_content">
|
||||||
<button id="id_dash_gear">
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_dash_applet_menu" %}
|
||||||
<i class="fa-solid fa-gear"></i>
|
|
||||||
</button>
|
|
||||||
{% include "apps/dashboard/_partials/_applets.html" %}
|
{% include "apps/dashboard/_partials/_applets.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% block content %}
|
||||||
<div class="gameboard-page">
|
<div class="gameboard-page">
|
||||||
<section id="id_applet_my_games">
|
{% include "apps/applets/_partials/_gear.html" with menu_id="id_game_applet_menu" %}
|
||||||
<h2>My Games</h2>
|
{% include "apps/gameboard/_partials/_applets.html" %}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% endblock scripts %}
|
{% endblock scripts %}
|
||||||
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
||||||
|
<script src="{% static "apps/scripts/applets.js" %}"></script>
|
||||||
<script>
|
<script>
|
||||||
document.body.addEventListener('htmx:configRequest', function(evt) {
|
document.body.addEventListener('htmx:configRequest', function(evt) {
|
||||||
evt.detail.headers['X-CSRFToken'] = getCookie('csrftoken');
|
evt.detail.headers['X-CSRFToken'] = getCookie('csrftoken');
|
||||||
|
|||||||
Reference in New Issue
Block a user