"""A small fast_app-style layout system. Instead of Django template inheritance (`{% extends "base.html" %}`), views build their page body with Python components and wrap it with `Page()` / `render_page()`. `Page()` is the equivalent of FastHTML's document wrapper: it hoists shared `` content (the `_HEADERS` block, analogous to `fast_app(hdrs=...)`), renders the navbar, and assembles the full document. """ import json from django.contrib.messages import get_messages from django.http import HttpRequest, HttpResponse from django.templatetags.static import static from django.urls import reverse from django.utils.html import conditional_escape from django.utils.safestring import SafeText, mark_safe from django_htmx.jinja import django_htmx_script from games.templatetags.version import version, version_date # Static head script that sets the dark/light class before paint (avoids FOUC). _THEME_FOUC_SCRIPT = """""" # The main module script: crown icon mount + theme-toggle wiring. # Split around the single dynamic value (game.mastered). _MAIN_SCRIPT_A = """""" # Toast notification region (Alpine.js). Verbatim from the old base.html. _TOAST_CONTAINER = """
""" def _main_script(mastered: bool) -> str: return _MAIN_SCRIPT_A + ("true" if mastered else "false") + _MAIN_SCRIPT_B def Navbar(*, today_played: str, last_7_played: str, current_year: int) -> SafeText: """Top navigation bar.""" logo = static("icons/schedule.png") return mark_safe(f"""""") def Page( content: SafeText | str, *, request: HttpRequest, title: str = "", scripts: SafeText | str = "", mastered: bool = False, ) -> SafeText: """Assemble a full HTML document around `content` (the fast_app equivalent).""" from games.views.general import global_current_year, model_counts counts = model_counts(request) year = global_current_year(request)["global_current_year"] navbar = Navbar( today_played=counts["today_played"], last_7_played=counts["last_7_played"], current_year=year, ) messages = [ {"message": str(m.message), "type": (m.tags or "info")} for m in get_messages(request) ] # Embed as JSON; guard against `` breaking out of the tag. messages_json = json.dumps(messages).replace("\n\n \n' ' \n' ' \n' ' \n' ' \n' f" Timetracker - {conditional_escape(title)}\n" f' \n' " \n" f' \n' f" {django_htmx_script(nonce=None)}\n" f' \n' ' \n' ' \n' ' \n' f" {_THEME_FOUC_SCRIPT}\n" " \n" ) body = ( ' \n' f' \n' f' loading indicator\n' '
\n' f" {navbar}\n" f'
{content}
\n' f' {version()} ({version_date()})\n' "
\n" f" {scripts}\n" f" {_main_script(mastered)}\n" ' \n' '
\n' f" {_TOAST_CONTAINER}\n" f' \n' " \n\n" ) return mark_safe(head + body) def render_page( request: HttpRequest, content: SafeText | str, *, title: str = "", scripts: SafeText | str = "", mastered: bool = False, status: int = 200, ) -> HttpResponse: """`render()`-style shortcut: build a full page and return an HttpResponse.""" return HttpResponse( Page(content, request=request, title=title, scripts=scripts, mastered=mastered), status=status, )