diff --git a/common/utils.py b/common/utils.py
index 264c9ca..ea2ea39 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -1,8 +1,9 @@
from random import choices
from string import ascii_lowercase
-from typing import Any
+from typing import Any, Callable
from django.template.loader import render_to_string
+from django.urls import NoReverseMatch, reverse
from django.utils.safestring import mark_safe
@@ -31,16 +32,68 @@ HTMLAttribute = tuple[str, str]
HTMLTag = str
-def A(attributes: list[HTMLAttribute], children: list[HTMLTag] | HTMLTag) -> HTMLTag:
+def Component(
+ attributes: list[HTMLAttribute] = [],
+ children: list[HTMLTag] | HTMLTag = [],
+ template: str = "",
+ tag_name: str = "",
+) -> HTMLTag:
+ if not tag_name and not template:
+ raise ValueError("One of template or tag_name is required.")
if isinstance(children, str):
children = [children]
childrenBlob = "\n".join(children)
attributesList = [f'{name} = "{value}"' for name, value in attributes]
attributesBlob = " ".join(attributesList)
- tag: str = f"{childrenBlob}"
+ tag: str = ""
+ if tag_name != "":
+ tag = f"{childrenBlob}"
+ elif template != "":
+ tag = render_to_string(
+ template,
+ {name: value for name, value in attributes} | {"slot": "\n".join(children)},
+ )
return mark_safe(tag)
+def A(
+ attributes: list[HTMLAttribute] = [],
+ children: list[HTMLTag] | HTMLTag = [],
+ url: str | Callable[..., Any] = "",
+):
+ """
+ Returns the HTML tag "a".
+ "url" can either be:
+ - URL (string)
+ - path name passed to reverse() (string)
+ - function
+ """
+ additional_attributes = []
+ if url:
+ if type(url) is str:
+ try:
+ url_result = reverse(url)
+ except NoReverseMatch:
+ url_result = url
+ elif callable(url):
+ url_result = url()
+ else:
+ raise TypeError("'url' is neither str nor function.")
+ additional_attributes = [("href", url_result)]
+ return Component(
+ tag_name="a", attributes=attributes + additional_attributes, children=children
+ )
+
+
+def Button(
+ attributes: list[HTMLAttribute] = [],
+ children: list[HTMLTag] | HTMLTag = [],
+):
+ return Component(
+ template="cotton/button.html", attributes=attributes, children=children
+ )
+
+
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
"""
Divides without triggering division by zero exception.
diff --git a/games/static/base.css b/games/static/base.css
index 7679a6d..ccaff9f 100644
--- a/games/static/base.css
+++ b/games/static/base.css
@@ -1,5 +1,5 @@
/*
-! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
+! tailwindcss v3.4.7 | MIT License | https://tailwindcss.com
*/
/*
@@ -1816,11 +1816,6 @@ input:checked + .toggle-bg {
border-color: rgb(220 215 254 / var(--tw-border-opacity));
}
-.\!bg-gray-50 {
- --tw-bg-opacity: 1 !important;
- background-color: rgb(249 250 251 / var(--tw-bg-opacity)) !important;
-}
-
.bg-blue-100 {
--tw-bg-opacity: 1;
background-color: rgb(225 239 254 / var(--tw-bg-opacity));
@@ -1966,6 +1961,10 @@ input:checked + .toggle-bg {
text-align: center;
}
+.text-right {
+ text-align: right;
+}
+
.align-top {
vertical-align: top;
}
@@ -2722,11 +2721,6 @@ textarea:disabled:is(.dark *) {
border-color: transparent;
}
-.dark\:\!bg-gray-700:is(.dark *) {
- --tw-bg-opacity: 1 !important;
- background-color: rgb(55 65 81 / var(--tw-bg-opacity)) !important;
-}
-
.dark\:bg-blue-200:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(195 221 253 / var(--tw-bg-opacity));
@@ -3075,6 +3069,10 @@ textarea:disabled:is(.dark *) {
--tw-space-x-reverse: 1;
}
+.rtl\:text-left:where([dir="rtl"], [dir="rtl"] *) {
+ text-align: left;
+}
+
.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
text-align: right;
}
diff --git a/games/templates/cotton/button.html b/games/templates/cotton/button.html
index 83f0695..3cc1f1f 100644
--- a/games/templates/cotton/button.html
+++ b/games/templates/cotton/button.html
@@ -1,4 +1,5 @@
diff --git a/games/templates/cotton/simple_table.html b/games/templates/cotton/simple_table.html
index 62c413f..8f08b99 100644
--- a/games/templates/cotton/simple_table.html
+++ b/games/templates/cotton/simple_table.html
@@ -2,6 +2,11 @@
+ {% if header_action %}
+
+ {{ header_action }}
+
+ {% endif %}
{% for column in columns %}{{ column }} | {% endfor %}
diff --git a/games/templates/cotton/table_header.html b/games/templates/cotton/table_header.html
new file mode 100644
index 0000000..2fa28cc
--- /dev/null
+++ b/games/templates/cotton/table_header.html
@@ -0,0 +1,3 @@
+
+ {{ slot }}
+
diff --git a/games/templates/list_purchases.html b/games/templates/list_purchases.html
index 8ea2bb0..d39e80a 100644
--- a/games/templates/list_purchases.html
+++ b/games/templates/list_purchases.html
@@ -5,6 +5,6 @@
{% endblock title %}
{% block content %}
-
+
{% endblock content %}
diff --git a/games/views/device.py b/games/views/device.py
index 7842a40..3fc1a2b 100644
--- a/games/views/device.py
+++ b/games/views/device.py
@@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
+from common.utils import A, Button
from games.forms import DeviceForm
from games.models import Device
from games.views.general import dateformat
@@ -35,6 +36,7 @@ def list_devices(request: HttpRequest) -> HttpResponse:
else None
),
"data": {
+ "header_action": A([], Button([], "Add device"), url="add_device"),
"columns": [
"Name",
"Type",
diff --git a/games/views/edition.py b/games/views/edition.py
index f569390..a2568cf 100644
--- a/games/views/edition.py
+++ b/games/views/edition.py
@@ -7,7 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
-from common.utils import A, truncate_with_popover
+from common.utils import A, Button, truncate_with_popover
from games.forms import EditionForm
from games.models import Edition, Game
from games.views.general import dateformat
@@ -36,6 +36,7 @@ def list_editions(request: HttpRequest) -> HttpResponse:
else None
),
"data": {
+ "header_action": A([], Button([], "Add edition"), url="add_edition"),
"columns": [
"Game",
"Name",
diff --git a/games/views/game.py b/games/views/game.py
index 51132e7..db12442 100644
--- a/games/views/game.py
+++ b/games/views/game.py
@@ -9,7 +9,7 @@ from django.template.loader import render_to_string
from django.urls import reverse
from common.time import format_duration
-from common.utils import A, safe_division, truncate_with_popover
+from common.utils import A, Button, safe_division, truncate_with_popover
from games.forms import GameForm
from games.models import Edition, Game, Purchase, Session
from games.views.general import (
@@ -45,6 +45,7 @@ def list_games(request: HttpRequest) -> HttpResponse:
else None
),
"data": {
+ "header_action": A([], Button([], "Add game"), url="add_game"),
"columns": [
"Name",
"Sort Name",
diff --git a/games/views/platform.py b/games/views/platform.py
index 49995b1..3906510 100644
--- a/games/views/platform.py
+++ b/games/views/platform.py
@@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.template.loader import render_to_string
from django.urls import reverse
+from common.utils import A, Button
from games.forms import PlatformForm
from games.models import Platform
from games.views.general import dateformat, use_custom_redirect
@@ -35,6 +36,7 @@ def list_platforms(request: HttpRequest) -> HttpResponse:
else None
),
"data": {
+ "header_action": A([], Button([], "Add platform"), url="add_platform"),
"columns": [
"Name",
"Group",
diff --git a/games/views/purchase.py b/games/views/purchase.py
index 87b4f77..9680948 100644
--- a/games/views/purchase.py
+++ b/games/views/purchase.py
@@ -13,7 +13,7 @@ from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
-from common.utils import A, truncate_with_popover
+from common.utils import A, Button, truncate_with_popover
from games.forms import PurchaseForm
from games.models import Edition, Purchase
from games.views.general import dateformat, use_custom_redirect
@@ -42,6 +42,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse:
else None
),
"data": {
+ "header_action": A([], Button([], "Add purchase"), url="add_purchase"),
"columns": [
"Name",
"Type",
diff --git a/games/views/session.py b/games/views/session.py
index 9022a32..0e85639 100644
--- a/games/views/session.py
+++ b/games/views/session.py
@@ -9,7 +9,7 @@ from django.urls import reverse
from django.utils import timezone
from common.time import format_duration
-from common.utils import A, truncate_with_popover
+from common.utils import A, Button, truncate_with_popover
from games.forms import SessionForm
from games.models import Purchase, Session
from games.views.general import (
@@ -45,6 +45,7 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
else None
),
"data": {
+ "header_action": A([], Button([], "Add session"), url="add_session"),
"columns": [
"Name",
"Date",
@@ -57,16 +58,11 @@ def list_sessions(request: HttpRequest) -> HttpResponse:
"rows": [
[
A(
- [
- (
- "href",
- reverse(
- "view_game",
- args=[session.purchase.edition.game.pk],
- ),
- )
- ],
- truncate_with_popover(session.purchase.edition.name),
+ children=truncate_with_popover(session.purchase.edition.name),
+ url=reverse(
+ "view_game",
+ args=[session.purchase.edition.game.pk],
+ ),
),
f"{session.timestamp_start.strftime(datetimeformat)}{f" — {session.timestamp_end.strftime(timeformat)}" if session.timestamp_end else ""}",
(