"""Domain components for games / purchases / sessions.""" from typing import Any 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.primitives import ( A, Div, Icon, Popover, PopoverTruncated, Span, ) from games.models import Game, Purchase, Session def GameLink( game_id: int, name: str = "", children: list[HTMLTag] | HTMLTag | None = None, ) -> SafeText: """Link to a game's detail page. Uses children (slot) if provided, otherwise name.""" from django.urls import reverse children = children or [] display = children if children else [name] link = reverse("games:view_game", args=[game_id]) return Span( attributes=[("class", "truncate-container")], children=[ Component( tag_name="a", attributes=[ ("href", link), ("class", "underline decoration-slate-500 sm:decoration-2"), ], children=display if isinstance(display, list) else [display], ), ], ) _STATUS_COLORS = { "u": "bg-gray-500", "p": "bg-orange-400", "f": "bg-green-500", "a": "bg-red-500", "r": "bg-purple-500", } def GameStatus( children: list[HTMLTag] | HTMLTag | None = None, status: str = "u", display: str = "", class_: str = "", ) -> SafeText: """Colored status dot with label. Status codes: u/p/f/a/r.""" children = children or [] outer_class = ( f"{'flex' if display == 'flex' else 'inline-flex'} " "gap-2 items-center align-middle" ) if class_: outer_class += f" {class_}" dot_color = _STATUS_COLORS.get(status, _STATUS_COLORS["u"]) dot = Span( attributes=[("class", f"rounded-xl w-3 h-3 {dot_color}")], children=["\xa0"], ) return Span( attributes=[("class", outer_class)], children=[dot] + (children if isinstance(children, list) else [children]), ) def PriceConverted( children: list[HTMLTag] | HTMLTag | None = None, ) -> SafeText: """Wrap content in a span that indicates the price was converted.""" children = children or [] return Span( attributes=[ ("title", "Price is a result of conversion and rounding."), ("class", "decoration-dotted underline"), ], children=children if isinstance(children, list) else [children], ) def LinkedPurchase(purchase: Purchase) -> SafeText: link = reverse("games:view_purchase", args=[int(purchase.id)]) link_content = "" popover_content = "" game_count = purchase.games.count() popover_if_not_truncated = False if game_count == 1: link_content += purchase.games.first().name popover_content = link_content if game_count > 1: if purchase.name: link_content += f"{purchase.name}" popover_content += f"

{purchase.name}


" else: link_content += f"{game_count} games" popover_if_not_truncated = True popover_content += f""" """ icon = ( (purchase.platform.icon if purchase.platform else "unspecified") if game_count == 1 else "unspecified" ) if link_content == "": raise ValueError("link_content is empty!!") a_content = Div( [("class", "inline-flex gap-2 items-center")], [ Icon( icon, [("title", "Multiple")], ), PopoverTruncated( input_string=link_content, popover_content=mark_safe(popover_content), popover_if_not_truncated=popover_if_not_truncated, ), ], ) return A(href=link, children=[a_content]) def NameWithIcon( name: str = "", game: Game | None = None, session: Session | None = None, linkify: bool = True, emulated: bool = False, ) -> SafeText: _name, platform, final_emulated, create_link, link = _resolve_name_with_icon( name, game, session, linkify ) content = Div( [("class", "inline-flex gap-2 items-center")], [ Icon( platform.icon, [("title", platform.name)], ) if platform else "", Icon("emulated", [("title", "Emulated")]) if final_emulated else "", PopoverTruncated(_name), ], ) return ( A( href=link, children=[content], ) if create_link else content ) def _resolve_name_with_icon( name: str, game: Game | None, session: Session | None, linkify: bool, ) -> tuple[str, Any, bool, bool, str]: create_link = False link = "" platform = None final_emulated = False if session is not None: game = session.game platform = game.platform final_emulated = session.emulated if linkify: create_link = True link = reverse("games:view_game", args=[int(game.pk)]) elif game is not None: platform = game.platform if linkify: create_link = True link = reverse("games:view_game", args=[int(game.pk)]) _name = name or (game.name if game else "") return _name, platform, final_emulated, create_link, link def PurchasePrice(purchase) -> SafeText: return Popover( popover_content=f"{floatformat(purchase.price)} {purchase.price_currency}", wrapped_content=f"{floatformat(purchase.converted_price)} {purchase.converted_currency}", wrapped_classes="underline decoration-dotted", ) def GameStatusSelector(game, game_statuses, csrf_token: str) -> SafeText: """Alpine.js dropdown to change a game's status.""" options_html = "\n".join( f"" for value, label in game_statuses ) list_items = "\n".join( f"
  • " f"{GameStatus(status=value, children=[label], display='flex', class_='text-slate-300')}" f"
  • " for value, label in game_statuses ) return mark_safe(f"""
    {_dropdown_button_html(options_html, list_items)}
    """) def SessionDeviceSelector(session, session_devices, csrf_token: str) -> SafeText: """Alpine.js dropdown to change a session's device.""" device_id = session.device_id or "null" device_name = (session.device.name if session.device else "Unknown").replace( "'", "\\'" ) list_items = "\n".join( f'
  • {d.name}
  • " for d in session_devices ) return mark_safe(f"""
    { _dropdown_button_html( '' + str(Icon("arrowdown")), list_items ) }
    """) def _dropdown_button_html(button_content: str, list_items: str) -> str: """Shared dropdown button + list structure for Alpine.js selectors.""" return ( '
    ' '" "
    " )