diff --git a/app.py b/app.py index 4054cb2..3eaf330 100644 --- a/app.py +++ b/app.py @@ -8,7 +8,7 @@ from jinja2 import FileSystemLoader, ChoiceLoader from shared.infrastructure.factory import create_base_app -from bp import register_calendars, register_markets, register_payments +from bp import register_calendars, register_markets, register_payments, register_page async def events_context() -> dict: @@ -55,6 +55,12 @@ def create_app() -> "Quart": app.jinja_loader, ]) + # Page summary: // — upcoming events across all calendars + app.register_blueprint( + register_page(), + url_prefix="/", + ) + # Calendars nested under post slug: //calendars/... app.register_blueprint( register_calendars(), diff --git a/bp/__init__.py b/bp/__init__.py index 5b4924b..6edd0fd 100644 --- a/bp/__init__.py +++ b/bp/__init__.py @@ -1,3 +1,4 @@ from .calendars.routes import register as register_calendars from .markets.routes import register as register_markets from .payments.routes import register as register_payments +from .page.routes import register as register_page diff --git a/bp/page/__init__.py b/bp/page/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bp/page/routes.py b/bp/page/routes.py new file mode 100644 index 0000000..0df9642 --- /dev/null +++ b/bp/page/routes.py @@ -0,0 +1,81 @@ +""" +Page summary blueprint — shows upcoming events across all calendars +for a container (e.g. the village hall). + +Routes: + GET // — full page with first page of entries + GET //entries — HTMX fragment for infinite scroll +""" +from __future__ import annotations + +from quart import Blueprint, g, request, render_template, make_response + +from shared.browser.app.utils.htmx import is_htmx_request +from shared.infrastructure.cart_identity import current_cart_identity +from shared.services.registry import services + + +def register() -> Blueprint: + bp = Blueprint("page_summary", __name__) + + async def _load_entries(post_id, page, per_page=20): + """Load upcoming entries + pending ticket counts for current user.""" + entries, has_more = await services.calendar.upcoming_entries_for_container( + g.s, "page", post_id, page=page, per_page=per_page, + ) + + # Pending ticket counts keyed by entry_id + ident = current_cart_identity() + pending_tickets = {} + if entries: + tickets = await services.calendar.pending_tickets( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + for t in tickets: + if t.entry_id is not None: + pending_tickets[t.entry_id] = pending_tickets.get(t.entry_id, 0) + 1 + + return entries, has_more, pending_tickets + + @bp.get("/") + async def index(): + post = g.post_data["post"] + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets = await _load_entries(post["id"], page) + + ctx = dict( + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page=page, + view=view, + ) + + if is_htmx_request(): + html = await render_template("_types/page_summary/_main_panel.html", **ctx) + else: + html = await render_template("_types/page_summary/index.html", **ctx) + + return await make_response(html, 200) + + @bp.get("/entries") + async def entries_fragment(): + post = g.post_data["post"] + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets = await _load_entries(post["id"], page) + + html = await render_template( + "_types/page_summary/_cards.html", + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page=page, + view=view, + ) + return await make_response(html, 200) + + return bp diff --git a/shared b/shared index 30b5a14..6e438db 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit 30b5a1438be5db6817c1a7ef8d3ee441165fb2dc +Subproject commit 6e438dbfdc03768ca75b45040883fd8c998aecce diff --git a/templates/_types/page_summary/_card.html b/templates/_types/page_summary/_card.html new file mode 100644 index 0000000..4f72af5 --- /dev/null +++ b/templates/_types/page_summary/_card.html @@ -0,0 +1,93 @@ +{# List card for page summary — one entry #} +
+
+ {# Left: event info #} +
+ {% set day_href = events_url('/' ~ post.slug ~ '/calendars/' ~ entry.calendar_slug ~ '/' ~ entry.start_at.strftime('%Y/%m/%d') ~ '/') %} + +

{{ entry.name }}

+
+ + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} + +
+ {{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
+ + {% if entry.cost %} +
+ £{{ '%.2f'|format(entry.cost) }} +
+ {% endif %} +
+ + {# Right: ticket widget #} + {% if entry.ticket_price is not none %} +
+ {% set qty = pending_tickets.get(entry.id, 0) %} + {% set ticket_url = cart_url('/ticket-quantity/') %} +
+ £{{ '%.2f'|format(entry.ticket_price) }} + + {% if qty == 0 %} +
+ + + + +
+ {% else %} +
+ + + + +
+ + + + + + {{ qty }} + + + + +
+ + + + +
+ {% endif %} +
+
+ {% endif %} +
+
diff --git a/templates/_types/page_summary/_card_tile.html b/templates/_types/page_summary/_card_tile.html new file mode 100644 index 0000000..8f4aa79 --- /dev/null +++ b/templates/_types/page_summary/_card_tile.html @@ -0,0 +1,84 @@ +{# Tile card for page summary — compact event tile #} +
+ {% set day_href = events_url('/' ~ post.slug ~ '/calendars/' ~ entry.calendar_slug ~ '/' ~ entry.start_at.strftime('%Y/%m/%d') ~ '/') %} + +
+

{{ entry.name }}

+ + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} + +
+ {{ entry.start_at.strftime('%a %-d %b') }} + · + {{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} – {{ entry.end_at.strftime('%H:%M') }}{% endif %} +
+ + {% if entry.cost %} +
+ £{{ '%.2f'|format(entry.cost) }} +
+ {% endif %} +
+
+ + {# Ticket widget below card #} + {% if entry.ticket_price is not none %} +
+ £{{ '%.2f'|format(entry.ticket_price) }}/ticket + + {% set qty = pending_tickets.get(entry.id, 0) %} + {% set ticket_url = cart_url('/ticket-quantity/') %} + + {% if qty == 0 %} +
+ + + + +
+ {% else %} +
+
+ + + + +
+ + {{ qty }} + +
+ + + + +
+
+ {% endif %} +
+ {% endif %} +
diff --git a/templates/_types/page_summary/_cards.html b/templates/_types/page_summary/_cards.html new file mode 100644 index 0000000..b6958ab --- /dev/null +++ b/templates/_types/page_summary/_cards.html @@ -0,0 +1,31 @@ +{% for entry in entries %} + {% if view == 'tile' %} + {% include "_types/page_summary/_card_tile.html" %} + {% else %} + {# Date header when date changes (list view only) #} + {% set entry_date = entry.start_at.strftime('%A %-d %B %Y') %} + {% if loop.first or entry_date != entries[loop.index0 - 1].start_at.strftime('%A %-d %B %Y') %} +
+

+ {{ entry_date }} +

+
+ {% endif %} + {% include "_types/page_summary/_card.html" %} + {% endif %} +{% endfor %} +{% if has_more %} + {# Infinite scroll sentinel #} + {% set entries_url = url_for('page_summary.entries_fragment', page=page + 1, view=view if view != 'list' else '')|host %} + +{% endif %} diff --git a/templates/_types/page_summary/_main_panel.html b/templates/_types/page_summary/_main_panel.html new file mode 100644 index 0000000..ab1a8b4 --- /dev/null +++ b/templates/_types/page_summary/_main_panel.html @@ -0,0 +1,54 @@ +{# View toggle bar - desktop only #} + + +{# Cards container - list or grid based on view #} +{% if entries %} + {% if view == 'tile' %} +
+ {% include "_types/page_summary/_cards.html" %} +
+ {% else %} +
+ {% include "_types/page_summary/_cards.html" %} +
+ {% endif %} +{% else %} +
+ +

No upcoming events

+
+{% endif %} +
diff --git a/templates/_types/page_summary/index.html b/templates/_types/page_summary/index.html new file mode 100644 index 0000000..d084317 --- /dev/null +++ b/templates/_types/page_summary/index.html @@ -0,0 +1,15 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block root_header_child %} + {% from '_types/root/_n/macros.html' import index_row with context %} + {% call index_row('post-header-child', '_types/post/header/_header.html') %} + {% block post_header_child %} + {% endblock %} + {% endcall %} +{% endblock %} + +{% block content %} + {% include '_types/page_summary/_main_panel.html' %} +{% endblock %}