diff --git a/common/components/__init__.py b/common/components/__init__.py index 635cc5c..0763b1f 100644 --- a/common/components/__init__.py +++ b/common/components/__init__.py @@ -4,8 +4,6 @@ Split into core / primitives / domain / filters submodules; this package re-exports the public API so ``from common.components import X`` keeps working. """ -from common.utils import truncate - from common.components.core import ( Component, HTMLAttribute, @@ -13,41 +11,6 @@ from common.components.core import ( _render_element, randomid, ) -from common.components.primitives import ( - A, - AddForm, - Button, - ButtonGroup, - CsrfInput, - Div, - ExternalScript, - H1, - Icon, - Input, - Modal, - ModuleScript, - Pill, - Popover, - PopoverTruncated, - SearchField, - SimpleTable, - Span, - Label, - TableHeader, - TableRow, - TableTd, - Template, - YearPicker, - paginated_table_content, -) -from common.components.search_select import ( - DEFAULT_PREFETCH, - FilterSelect, - LabeledOption, - SearchSelect, - SearchSelectOption, - searchselect_selected, -) from common.components.domain import ( GameLink, GameStatus, @@ -60,13 +23,53 @@ from common.components.domain import ( _resolve_name_with_icon, ) from common.components.filters import ( - FilterBar, - PurchaseFilterBar, - SessionFilterBar, DeviceFilterBar, + FilterBar, PlatformFilterBar, PlayEventFilterBar, + PurchaseFilterBar, + SessionFilterBar, ) +from common.components.primitives import ( + H1, + A, + AddForm, + Button, + ButtonGroup, + CsrfInput, + Div, + ExternalScript, + Icon, + Input, + Label, + Li, + Modal, + ModuleScript, + Pill, + Popover, + PopoverTruncated, + SearchField, + SimpleTable, + Span, + TableHeader, + TableRow, + TableTd, + Td, + Template, + Tr, + Ul, + YearPicker, + paginated_table_content, +) +from common.components.search_select import ( + DEFAULT_PREFETCH, + FilterSelect, + LabeledOption, + SearchSelect, + SearchSelectOption, + searchselect_selected, +) +from common.utils import truncate __all__ = [ "truncate", diff --git a/common/components/domain.py b/common/components/domain.py index ee4deda..977946c 100644 --- a/common/components/domain.py +++ b/common/components/domain.py @@ -6,7 +6,7 @@ from django.template.defaultfilters import floatformat from django.urls import reverse from django.utils.safestring import SafeText, mark_safe -from common.components.core import Component, HTMLTag +from common.components.core import HTMLTag from common.components.primitives import ( A, Div, @@ -33,10 +33,9 @@ def GameLink( return Span( attributes=[("class", "truncate-container")], children=[ - Component( - tag_name="a", + A( + href=link, attributes=[ - ("href", link), ("class", "underline decoration-slate-500 sm:decoration-2"), ], children=display if isinstance(display, list) else [display], diff --git a/common/components/filters.py b/common/components/filters.py index 20261eb..e25038e 100644 --- a/common/components/filters.py +++ b/common/components/filters.py @@ -6,8 +6,12 @@ from django.db import models from django.utils.safestring import SafeText, mark_safe from common.components.core import Component -from common.components.primitives import Label, Span -from common.components.search_select import DEFAULT_PREFETCH, FilterSelect, LabeledOption +from common.components.primitives import Div, Input, Label, Span +from common.components.search_select import ( + DEFAULT_PREFETCH, + FilterSelect, + LabeledOption, +) class FilterChoice(NamedTuple): @@ -206,8 +210,7 @@ def _filter_mins_to_hrs(val) -> str: def _filter_field(label: str, widget) -> SafeText: """A labelled filter field:
{widget}
.""" - return Component( - tag_name="div", + return Div( attributes=[("class", "flex flex-col gap-1")], children=[ Label( @@ -223,8 +226,7 @@ def _filter_checkbox(name: str, label: str, checked: bool) -> SafeText: return Label( attributes=[("class", "flex items-center gap-2 text-sm text-heading")], children=[ - Component( - tag_name="input", + Input( attributes=[ ("type", "checkbox"), ("name", name), @@ -283,13 +285,11 @@ def RangeSlider( point_mode = bool(min_value and max_value and min_value == max_value) initial_mode = "point" if point_mode else "range" - return Component( - tag_name="div", + return Div( attributes=[("class", "range-slider-block mb-4")], children=[ # ── Label row ── - Component( - tag_name="div", + Div( attributes=[("class", "flex items-center gap-2 mb-1")], children=[ Label( @@ -299,8 +299,7 @@ def RangeSlider( ], children=[label], ), - Component( - tag_name="input", + Input( attributes=[ ("type", "number"), ("name", min_input_id), @@ -324,8 +323,7 @@ def RangeSlider( ], children=["–"], ), - Component( - tag_name="input", + Input( attributes=[ ("type", "number"), ("name", max_input_id), @@ -379,8 +377,7 @@ def RangeSlider( ], ), # ── Slider row ── - Component( - tag_name="div", + Div( attributes=[ ("class", "range-slider relative h-10 select-none mt-1"), ("data-mode", initial_mode), @@ -389,8 +386,7 @@ def RangeSlider( ("data-step", str(step)), ], children=[ - Component( - tag_name="div", + Div( attributes=[ ( "class", @@ -399,8 +395,7 @@ def RangeSlider( ), ], ), - Component( - tag_name="div", + Div( attributes=[ ( "class", @@ -411,8 +406,7 @@ def RangeSlider( ], ), # Min handle (hidden in point mode via JS) - Component( - tag_name="div", + Div( attributes=[ ( "class", @@ -429,8 +423,7 @@ def RangeSlider( ], ), # Max handle - Component( - tag_name="div", + Div( attributes=[ ( "class", @@ -480,8 +473,7 @@ def _filter_collapse_button() -> SafeText: def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText: - return Component( - tag_name="div", + return Div( attributes=[("class", "flex gap-3 items-center")], children=[ Component( @@ -521,8 +513,7 @@ def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText: ("id", "save-preset-area"), ], children=[ - Component( - tag_name="input", + Input( attributes=[ ("type", "text"), ("id", "preset-name-input"), @@ -572,8 +563,7 @@ def _filter_action_row(preset_list_url: str, preset_save_url: str) -> SafeText: ), ], ), - Component( - tag_name="div", + Div( attributes=[ ("id", "preset-dropdown"), ("class", "relative"), @@ -594,13 +584,11 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe """Shared collapsible filter-bar chrome. `fields` is the per-entity body (grids, sliders, checkboxes); the shell adds the collapse toggle, the form, the hidden filter-json input and the Apply/Clear/preset action row.""" - return Component( - tag_name="div", + return Div( attributes=[("id", "filter-bar"), ("class", "mb-6")], children=[ _filter_collapse_button(), - Component( - tag_name="div", + Div( attributes=[ ("id", "filter-bar-body"), ( @@ -617,8 +605,7 @@ def _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -> SafeTe ("onsubmit", "return applyFilterBar(event)"), ], children=[ - Component( - tag_name="input", + Input( attributes=[ ("type", "hidden"), ("id", _FILTER_INPUT_ID), @@ -725,8 +712,7 @@ def FilterBar( price_range_max = max(int(price_aggregate.get("price_max") or 100), 1) fields = [ - Component( - tag_name="div", + Div( attributes=[("class", _FILTER_GRID_CLASS)], children=[ _filter_field( @@ -817,8 +803,7 @@ def FilterBar( min_placeholder="e.g. 1985", max_placeholder="e.g. 2010", ), - Component( - tag_name="div", + Div( attributes=[("class", "flex items-end gap-4 mb-4 flex-wrap")], children=[ _filter_checkbox("filter-mastered", "Mastered", mastered_value), @@ -970,8 +955,7 @@ def SessionFilterBar( duration_range_max = 200 fields = [ - Component( - tag_name="div", + Div( attributes=[("class", _FILTER_GRID_CLASS)], children=[ _filter_field( @@ -1027,8 +1011,7 @@ def SessionFilterBar( min_placeholder="e.g. 30", max_placeholder="e.g. 180", ), - Component( - tag_name="div", + Div( attributes=[("class", "flex gap-4 mb-4")], children=[ _filter_checkbox("filter-emulated", "Emulated", emulated_value), @@ -1079,8 +1062,7 @@ def PurchaseFilterBar( num_range_min, num_range_max = 0, 10 fields = [ - Component( - tag_name="div", + Div( attributes=[("class", _FILTER_GRID_CLASS)], children=[ _filter_field( @@ -1127,42 +1109,48 @@ def PurchaseFilterBar( ), ], ), - Component( - tag_name="div", + Div( attributes=[("class", "flex items-end gap-4 mb-4")], children=[ _filter_checkbox("filter-refunded", "Refunded", is_refunded_value), _filter_checkbox("filter-infinite", "Infinite", infinite_value), - _filter_checkbox("filter-needs-price-update", "Needs Price Update", needs_price_update_value), + _filter_checkbox( + "filter-needs-price-update", + "Needs Price Update", + needs_price_update_value, + ), ], ), - Component( - tag_name="div", + Div( attributes=[("class", _FILTER_GRID_CLASS)], children=[ _filter_field( "Original Currency", - Component( - tag_name="input", + Input( attributes=[ ("type", "text"), ("name", "filter-price_currency"), ("value", price_currency_value), ("placeholder", "e.g. USD, EUR"), - ("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), + ( + "class", + "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body", + ), ], ), ), _filter_field( "Converted Currency", - Component( - tag_name="input", + Input( attributes=[ ("type", "text"), ("name", "filter-converted_currency"), ("value", converted_currency_value), ("placeholder", "e.g. USD, EUR"), - ("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), + ( + "class", + "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body", + ), ], ), ), @@ -1193,9 +1181,7 @@ def PurchaseFilterBar( return _filter_bar(fields, filter_json, preset_list_url, preset_save_url) -def DeviceFilterBar( - filter_json="", preset_list_url="", preset_save_url="" -) -> SafeText: +def DeviceFilterBar(filter_json="", preset_list_url="", preset_save_url="") -> SafeText: """Collapsible filter bar for the Device list.""" from games.models import Device @@ -1204,8 +1190,7 @@ def DeviceFilterBar( type_choice = _filter_get_choice(existing, "type") fields = [ - Component( - tag_name="div", + Div( attributes=[("class", _FILTER_GRID_CLASS)], children=[ _filter_field( @@ -1233,33 +1218,36 @@ def PlatformFilterBar( group_value = existing.get("group", {}).get("value", "") fields = [ - Component( - tag_name="div", + Div( attributes=[("class", _FILTER_GRID_CLASS)], children=[ _filter_field( "Platform Name", - Component( - tag_name="input", + Input( attributes=[ ("type", "text"), ("name", "filter-name"), ("value", name_value), ("placeholder", "e.g. Nintendo Switch"), - ("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), + ( + "class", + "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body", + ), ], ), ), _filter_field( "Platform Group", - Component( - tag_name="input", + Input( attributes=[ ("type", "text"), ("name", "filter-group"), ("value", group_value), ("placeholder", "e.g. Nintendo"), - ("class", "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body"), + ( + "class", + "w-full rounded border-default-medium p-2 bg-neutral-secondary-medium text-body", + ), ], ), ), @@ -1278,8 +1266,7 @@ def PlayEventFilterBar( days_min, days_max = _parse_range(existing, "days_to_finish") fields = [ - Component( - tag_name="div", + Div( attributes=[("class", _FILTER_GRID_CLASS)], children=[ _filter_field( diff --git a/common/components/primitives.py b/common/components/primitives.py index 1828544..f88bd1f 100644 --- a/common/components/primitives.py +++ b/common/components/primitives.py @@ -6,10 +6,9 @@ from django.urls import reverse from django.utils.html import conditional_escape from django.utils.safestring import SafeText, mark_safe +from common.components.core import Component, HTMLAttribute, HTMLTag, randomid from common.icons import get_icon from common.utils import truncate -from common.components.core import Component, HTMLAttribute, HTMLTag, randomid - _COLOR_CLASSES = { "blue": "text-white bg-brand box-border border border-transparent hover:bg-brand-strong focus:ring-4 focus:ring-brand-medium", @@ -57,8 +56,7 @@ def _popover_html( "dark:bg-purple-800" ) - div = Component( - tag_name="div", + div = Div( attributes=[ ("data-popover", ""), ("id", id), @@ -66,12 +64,11 @@ def _popover_html( ("class", popover_tooltip_class), ], children=[ - Component( - tag_name="div", + Div( attributes=[("class", "px-3 py-2")], children=[popover_content], ), - Component(tag_name="div", attributes=[("data-popper-arrow", "")]), + Div(attributes=[("data-popper-arrow", "")]), mark_safe( # nosec — intentional HTML comment for Tailwind JIT "" @@ -323,8 +320,7 @@ def ButtonGroup(buttons: list[dict] | None = None) -> SafeText: ) ) - return Component( - tag_name="div", + return Div( attributes=[("class", "inline-flex rounded-md shadow-xs"), ("role", "group")], children=children, ) @@ -339,6 +335,42 @@ def Div( return Component(tag_name="div", attributes=attributes, children=children) +def P( + attributes: list[HTMLAttribute] | None = None, + children: list[HTMLTag] | HTMLTag | None = None, +) -> SafeText: + attributes = attributes or [] + children = children or [] + return Component(tag_name="p", attributes=attributes, children=children) + + +def Ul( + attributes: list[HTMLAttribute] | None = None, + children: list[HTMLTag] | HTMLTag | None = None, +) -> SafeText: + attributes = attributes or [] + children = children or [] + return Component(tag_name="ul", attributes=attributes, children=children) + + +def Li( + attributes: list[HTMLAttribute] | None = None, + children: list[HTMLTag] | HTMLTag | None = None, +) -> SafeText: + attributes = attributes or [] + children = children or [] + return Component(tag_name="li", attributes=attributes, children=children) + + +def Strong( + attributes: list[HTMLAttribute] | None = None, + children: list[HTMLTag] | HTMLTag | None = None, +) -> SafeText: + attributes = attributes or [] + children = children or [] + return Component(tag_name="strong", attributes=attributes, children=children) + + def Input( type: str = "text", attributes: list[HTMLAttribute] | None = None, @@ -600,8 +632,7 @@ def SearchField( ], children=["Search"], ), - Component( - tag_name="div", + Div( attributes=[("class", "relative")], children=[ mark_safe( @@ -612,10 +643,9 @@ def SearchField( 'd="m21 21-3.5-3.5M17 10a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/>' "" ), - Component( - tag_name="input", + Input( + type="search", attributes=[ - ("type", "search"), ("id", id), ("name", id), ("value", search_string), @@ -687,8 +717,7 @@ def Modal( ) -> SafeText: """Modal overlay with container. Content (form, buttons) goes in children.""" children = children or [] - outer = Component( - tag_name="div", + outer = Div( attributes=[ ("id", modal_id), ( @@ -698,8 +727,7 @@ def Modal( ), ], children=[ - Component( - tag_name="div", + Div( attributes=[ ( "class", @@ -714,13 +742,39 @@ def Modal( return mark_safe(str(outer)) +def Td( + attributes: list[HTMLAttribute] | None = None, + children: list[HTMLTag] | HTMLTag | None = None, +) -> SafeText: + attributes = attributes or [] + children = children or [] + return Component(tag_name="td", attributes=attributes, children=children) + + +def Tr( + attributes: list[HTMLAttribute] | None = None, + children: list[HTMLTag] | HTMLTag | None = None, +) -> SafeText: + attributes = attributes or [] + children = children or [] + return Component(tag_name="tr", attributes=attributes, children=children) + + +def Th( + attributes: list[HTMLAttribute] | None = None, + children: list[HTMLTag] | HTMLTag | None = None, +) -> SafeText: + attributes = attributes or [] + children = children or [] + return Component(tag_name="th", attributes=attributes, children=children) + + def TableTd( children: list[HTMLTag] | HTMLTag | None = None, ) -> SafeText: """Styled table cell.""" children = children or [] - return Component( - tag_name="td", + return Td( attributes=[("class", "px-6 py-4 min-w-20-char max-w-20-char")], children=children if isinstance(children, list) else [children], ) @@ -765,8 +819,7 @@ def TableRow(data: dict | list | None = None) -> SafeText: for i, cell in enumerate(cells): if i == 0: cell_elements.append( - Component( - tag_name="th", + Th( attributes=[ ("scope", "row"), ( @@ -781,7 +834,7 @@ def TableRow(data: dict | list | None = None) -> SafeText: else: cell_elements.append(TableTd(children=[cell])) - return Component(tag_name="tr", attributes=tr_attrs, children=cell_elements) + return Tr(attributes=tr_attrs, children=cell_elements) def Icon( diff --git a/games/views/auth.py b/games/views/auth.py index 1839d81..7ee87f5 100644 --- a/games/views/auth.py +++ b/games/views/auth.py @@ -6,6 +6,7 @@ from django.http import HttpResponse from django.utils.safestring import SafeText, mark_safe from common.components import Component, CsrfInput, Div, Input +from common.components.primitives import Td, Tr from common.layout import render_page @@ -15,12 +16,10 @@ def _login_content(form, request) -> SafeText: children=[ CsrfInput(request), mark_safe(str(form.as_table())), - Component( - tag_name="tr", + Tr( children=[ - Component(tag_name="td"), - Component( - tag_name="td", + Td(), + Td( children=[ Input(type="submit", attributes=[("value", "Login")]) ], diff --git a/games/views/game.py b/games/views/game.py index 12ae14a..18fe644 100644 --- a/games/views/game.py +++ b/games/views/game.py @@ -2,15 +2,16 @@ from typing import Any from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator -from django.middleware.csrf import get_token from django.db.models import Q from django.http import HttpRequest, HttpResponse, HttpResponseRedirect +from django.middleware.csrf import get_token from django.shortcuts import get_object_or_404, redirect from django.template.defaultfilters import date as date_filter from django.urls import reverse from django.utils.safestring import SafeText, mark_safe from common.components import ( + H1, A, AddForm, Button, @@ -21,9 +22,7 @@ from common.components import ( FilterBar, GameStatus, GameStatusSelector, - H1, Icon, - SearchField, LinkedPurchase, Modal, ModuleScript, @@ -31,9 +30,12 @@ from common.components import ( Popover, PopoverTruncated, PurchasePrice, + SearchField, SimpleTable, + Ul, paginated_table_content, ) +from common.components.primitives import Li, Span, Strong from common.icons import get_icon from common.layout import render_page from common.time import ( @@ -193,19 +195,13 @@ def _delete_game_confirmation_modal( ) -> SafeText: data_items = [] if session_count: - data_items.append( - Component(tag_name="li", children=[f"{session_count} session(s)"]) - ) + data_items.append(Li(children=[f"{session_count} session(s)"])) if purchase_count: - data_items.append( - Component(tag_name="li", children=[f"{purchase_count} purchase(s)"]) - ) + data_items.append(Li(children=[f"{purchase_count} purchase(s)"])) if playevent_count: - data_items.append( - Component(tag_name="li", children=[f"{playevent_count} play event(s)"]) - ) + data_items.append(Li(children=[f"{playevent_count} play event(s)"])) if not (session_count or purchase_count or playevent_count): - data_items.append(Component(tag_name="li", children=["No associated data"])) + data_items.append(Li(children=["No associated data"])) form = Component( tag_name="form", @@ -218,8 +214,7 @@ def _delete_game_confirmation_modal( ], children=[ CsrfInput(request), - Component( - tag_name="p", + P( attributes=[ ( "class", @@ -231,8 +226,7 @@ def _delete_game_confirmation_modal( "This will permanently delete this game and all associated data:" ], ), - Component( - tag_name="ul", + Ul( attributes=[ ( "class", @@ -242,8 +236,7 @@ def _delete_game_confirmation_modal( ], children=data_items, ), - Component( - tag_name="p", + P( attributes=[ ( "class", @@ -279,8 +272,7 @@ def _delete_game_confirmation_modal( return Modal( "delete-game-confirmation-modal", children=[ - Component( - tag_name="h1", + P( attributes=[ ( "class", @@ -289,12 +281,11 @@ def _delete_game_confirmation_modal( ], children=["Delete Game"], ), - Component( - tag_name="p", + P( attributes=[("class", "dark:text-white text-center mt-5")], children=[ "Are you sure you want to delete ", - Component(tag_name="strong", children=[game.name]), + Strong(children=[game.name]), "?", ], ), @@ -427,9 +418,7 @@ def _meta_row( label: str, value: SafeText | str, extra: SafeText | str = "" ) -> SafeText: children: list[SafeText | str] = [ - Component( - tag_name="span", attributes=[("class", "uppercase")], children=[label] - ), + Span(attributes=[("class", "uppercase")], children=[label]), value, ] if extra: @@ -452,9 +441,8 @@ def _game_action_buttons(game: Game) -> SafeText: "dark:text-white dark:hover:text-white dark:hover:bg-red-700 " "dark:focus:ring-blue-500 dark:focus:text-white hover:cursor-pointer" ) - edit_link = Component( - tag_name="a", - attributes=[("href", reverse("games:edit_game", args=[game.id]))], + edit_link = A( + href=reverse("games:edit_game", args=[game.id]), children=[ Component( tag_name="button", @@ -463,10 +451,9 @@ def _game_action_buttons(game: Game) -> SafeText: ) ], ) - delete_link = Component( - tag_name="a", + delete_link = A( + href="#", attributes=[ - ("href", "#"), ("hx-get", reverse("games:delete_game_confirmation", args=[game.id])), ("hx-target", "#global-modal-container"), ], @@ -499,21 +486,16 @@ def _game_history(statuschanges) -> SafeText: status=change.new_status, children=[change.get_new_status_display()], ) - edit = Component( - tag_name="a", - attributes=[("href", reverse("games:edit_statuschange", args=[change.id]))], + edit = A( + href=reverse("games:edit_statuschange", args=[change.id]), children=["Edit"], ) - delete = Component( - tag_name="a", - attributes=[ - ("href", reverse("games:delete_statuschange", args=[change.id])) - ], + delete = A( + href=reverse("games:delete_statuschange", args=[change.id]), children=["Delete"], ) items.append( - Component( - tag_name="li", + Li( attributes=[("class", "text-slate-500")], children=[ f"{prefix} status from ", @@ -528,8 +510,7 @@ def _game_history(statuschanges) -> SafeText: ], ) ) - return Component( - tag_name="ul", + return Ul( attributes=[("class", "list-disc list-inside")], children=items, ) @@ -576,12 +557,10 @@ def _game_overview_metrics(game: Game) -> dict[str, Any]: def _game_header(game: Game, request: HttpRequest, metrics: dict[str, Any]) -> SafeText: grey_value_class = "text-black dark:text-slate-300" - title_span = Component( - tag_name="span", + title_span = Span( attributes=[("class", "text-balance max-w-120 text-4xl")], children=[ - Component( - tag_name="span", + Span( attributes=[("class", "font-bold font-serif")], children=[game.name], ), @@ -634,8 +613,7 @@ def _game_header(game: Game, request: HttpRequest, metrics: dict[str, Any]) -> S [ _meta_row( "Original year", - Component( - tag_name="span", + Span( attributes=[("class", grey_value_class)], children=[str(game.original_year_released)], ), @@ -648,8 +626,7 @@ def _game_header(game: Game, request: HttpRequest, metrics: dict[str, Any]) -> S _played_row(game, request), _meta_row( "Platform", - Component( - tag_name="span", + Span( attributes=[("class", grey_value_class)], children=[str(game.platform)], ), diff --git a/games/views/purchase.py b/games/views/purchase.py index 9b292f9..2707e7f 100644 --- a/games/views/purchase.py +++ b/games/views/purchase.py @@ -6,13 +6,12 @@ from django.http import ( HttpResponseRedirect, ) from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse -from django.utils import timezone -from django.views.decorators.http import require_POST - from django.template.defaultfilters import date as date_filter from django.template.defaultfilters import floatformat +from django.urls import reverse +from django.utils import timezone from django.utils.safestring import SafeText, mark_safe +from django.views.decorators.http import require_POST from common.components import ( A, @@ -32,6 +31,7 @@ from common.components import ( TableRow, paginated_table_content, ) +from common.components.primitives import Li, P, Td, Tr, Ul from common.layout import render_page from common.time import dateformat from common.utils import paginate @@ -129,7 +129,7 @@ def list_purchases(request: HttpRequest) -> HttpResponse: elided_page_range=elided_page_range, request=request, ) - from common.components import PurchaseFilterBar, ModuleScript + from common.components import ModuleScript, PurchaseFilterBar filter_bar = PurchaseFilterBar( filter_json=filter_json, @@ -149,12 +149,10 @@ def list_purchases(request: HttpRequest) -> HttpResponse: def _purchase_additional_row() -> SafeText: """The 'Submit & Create Session' row shown below the main Submit button.""" - return Component( - tag_name="tr", + return Tr( children=[ - Component(tag_name="td"), - Component( - tag_name="td", + Td(), + Td( children=[ Button( [], @@ -262,8 +260,7 @@ def _view_purchase_content(purchase: Purchase) -> SafeText: Div( [("class", row_class)], [ - Component( - tag_name="p", + P( children=[ "Price per game: ", PriceConverted([floatformat(purchase.price_per_game, 0)]), @@ -273,10 +270,9 @@ def _view_purchase_content(purchase: Purchase) -> SafeText: ], ), Div([("class", row_class)], ["Games included in this purchase:"]), - Component( - tag_name="ul", + Ul( children=[ - Component(tag_name="li", children=[GameLink(game.id, game.name)]) + Li(children=[GameLink(game.id, game.name)]) for game in purchase.games.all() ], ), @@ -317,8 +313,7 @@ def _refund_confirmation_modal(purchase_id: int, request: HttpRequest) -> SafeTe ], children=[ CsrfInput(request), - Component( - tag_name="p", + P( attributes=[("class", "dark:text-white text-center mt-3 text-sm")], children=["Games will be marked as abandoned."], ), @@ -356,8 +351,7 @@ def _refund_confirmation_modal(purchase_id: int, request: HttpRequest) -> SafeTe ], children=["Confirm Refund"], ), - Component( - tag_name="p", + P( attributes=[("class", "dark:text-white text-center mt-5")], children=["Are you sure you want to mark this purchase as refunded?"], ), @@ -408,8 +402,10 @@ def related_purchase_by_game(request: HttpRequest) -> HttpResponse: from games.forms import related_purchase_queryset form = PurchaseForm() - qs = related_purchase_queryset().filter(games__in=games).order_by( - "games__sort_name" + qs = ( + related_purchase_queryset() + .filter(games__in=games) + .order_by("games__sort_name") ) form.fields["related_purchase"].queryset = qs diff --git a/games/views/session.py b/games/views/session.py index becdaa3..6ebc63b 100644 --- a/games/views/session.py +++ b/games/views/session.py @@ -15,7 +15,6 @@ from common.components import ( AddForm, Button, ButtonGroup, - Component, Div, Icon, ModuleScript, @@ -25,6 +24,7 @@ from common.components import ( SessionDeviceSelector, paginated_table_content, ) +from common.components.primitives import Span, Td, Tr from common.layout import render_page from common.time import ( dateformat, @@ -208,8 +208,7 @@ def _session_fields(form) -> SafeText: this_side = "start" if field.name == "timestamp_start" else "end" other_side = "end" if field.name == "timestamp_start" else "start" children.append( - Component( - tag_name="span", + Span( attributes=[ ( "class", @@ -292,8 +291,8 @@ def edit_session(request: HttpRequest, session_id: int) -> HttpResponse: def _session_row_fragment(session: Session) -> SafeText: """A single session (the old list_sessions.html#session-row partial), returned by the inline end/clone-session HTMX endpoints.""" - name_link = Component( - tag_name="a", + name_link = A( + href=reverse("games:view_game", args=[session.game.id]), attributes=[ ( "class", @@ -305,12 +304,10 @@ def _session_row_fragment(session: Session) -> SafeText: "group-hover:outline-purple-400 group-hover:outline-4 " "group-hover:decoration-purple-900 group-hover:text-purple-100", ), - ("href", reverse("games:view_game", args=[session.game.id])), ], children=[session.game.name], ) - name_td = Component( - tag_name="td", + name_td = Td( attributes=[ ( "class", @@ -319,15 +316,13 @@ def _session_row_fragment(session: Session) -> SafeText: ) ], children=[ - Component( - tag_name="span", + Span( attributes=[("class", "inline-block relative")], children=[name_link], ) ], ) - start_td = Component( - tag_name="td", + start_td = Td( attributes=[ ("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden sm:table-cell") ], @@ -336,10 +331,9 @@ def _session_row_fragment(session: Session) -> SafeText: if not session.timestamp_end: end_url = reverse("games:list_sessions_end_session", args=[session.id]) - end_inner: SafeText | str = Component( - tag_name="a", + end_inner: SafeText | str = A( + href=end_url, attributes=[ - ("href", end_url), ("hx-get", end_url), ("hx-target", "closest tr"), ("hx-swap", "outerHTML"), @@ -351,8 +345,7 @@ def _session_row_fragment(session: Session) -> SafeText: ), ], children=[ - Component( - tag_name="span", + Span( attributes=[("class", "text-yellow-300")], children=["Finish now?"], ) @@ -362,19 +355,17 @@ def _session_row_fragment(session: Session) -> SafeText: end_inner = "--" else: end_inner = date_filter(session.timestamp_end, "d/m/Y H:i") - end_td = Component( - tag_name="td", + end_td = Td( attributes=[ ("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono hidden lg:table-cell") ], children=[end_inner], ) - duration_td = Component( - tag_name="td", + duration_td = Td( attributes=[("class", "px-2 sm:px-4 md:px-6 md:py-2 font-mono")], children=[session.duration_formatted()], ) - return Component(tag_name="tr", children=[name_td, start_td, end_td, duration_td]) + return Tr(children=[name_td, start_td, end_td, duration_td]) def clone_session_by_id(session_id: int) -> Session: diff --git a/games/views/statuschange.py b/games/views/statuschange.py index bc81c28..994d49f 100644 --- a/games/views/statuschange.py +++ b/games/views/statuschange.py @@ -13,6 +13,7 @@ from common.components import ( Div, paginated_table_content, ) +from common.components.primitives import P from common.layout import render_page from common.time import dateformat, local_strftime from common.utils import paginate @@ -75,8 +76,7 @@ def _delete_statuschange_content(statuschange, request: HttpRequest) -> SafeText inner = Div( [], [ - Component( - tag_name="p", + P( children=["Are you sure you want to delete this status change?"], ), Button(