From 424c267110f1bd60b074e72c72de0529702b4c7c Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 22 Feb 2026 23:13:14 +0000 Subject: [PATCH] Add global all-events view at / and scope page summary to single page Root / shows all upcoming events across all pages with page badges. // reverted to show only that page's events. Co-Authored-By: Claude Opus 4.6 --- app.py | 8 +- bp/__init__.py | 1 + bp/all_events/__init__.py | 0 bp/all_events/routes.py | 143 +++++++++++++++++++ bp/page/routes.py | 36 ++--- templates/_types/all_events/_card.html | 62 ++++++++ templates/_types/all_events/_card_tile.html | 60 ++++++++ templates/_types/all_events/_cards.html | 31 ++++ templates/_types/all_events/_main_panel.html | 54 +++++++ templates/_types/all_events/index.html | 7 + 10 files changed, 377 insertions(+), 25 deletions(-) create mode 100644 bp/all_events/__init__.py create mode 100644 bp/all_events/routes.py create mode 100644 templates/_types/all_events/_card.html create mode 100644 templates/_types/all_events/_card_tile.html create mode 100644 templates/_types/all_events/_cards.html create mode 100644 templates/_types/all_events/_main_panel.html create mode 100644 templates/_types/all_events/index.html diff --git a/app.py b/app.py index 3eaf330..eb4da88 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, register_page +from bp import register_all_events, register_calendars, register_markets, register_payments, register_page async def events_context() -> dict: @@ -55,6 +55,12 @@ def create_app() -> "Quart": app.jinja_loader, ]) + # All events: / — global view across all pages + app.register_blueprint( + register_all_events(), + url_prefix="/", + ) + # Page summary: // — upcoming events across all calendars app.register_blueprint( register_page(), diff --git a/bp/__init__.py b/bp/__init__.py index 6edd0fd..3d06c7c 100644 --- a/bp/__init__.py +++ b/bp/__init__.py @@ -1,3 +1,4 @@ +from .all_events.routes import register as register_all_events from .calendars.routes import register as register_calendars from .markets.routes import register as register_markets from .payments.routes import register as register_payments diff --git a/bp/all_events/__init__.py b/bp/all_events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bp/all_events/routes.py b/bp/all_events/routes.py new file mode 100644 index 0000000..58732b8 --- /dev/null +++ b/bp/all_events/routes.py @@ -0,0 +1,143 @@ +""" +All-events blueprint — shows upcoming events across ALL pages' calendars. + +Mounted at / (root of events app). No slug context — works independently +of the post/slug machinery. + +Routes: + GET / — full page with first page of entries + GET /all-entries — HTMX fragment for infinite scroll + POST /all-tickets/adjust — adjust ticket quantity inline +""" +from __future__ import annotations + +from quart import Blueprint, g, request, render_template, render_template_string, 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("all_events", __name__) + + async def _load_entries(page, per_page=20): + """Load all upcoming entries + pending ticket counts + page info.""" + entries, has_more = await services.calendar.upcoming_entries_for_container( + g.s, 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 + + # Batch-load page info for container_ids + page_info = {} # {post_id: {title, slug}} + if entries: + post_ids = list({ + e.calendar_container_id + for e in entries + if e.calendar_container_type == "page" and e.calendar_container_id + }) + if post_ids: + posts = await services.blog.get_posts_by_ids(g.s, post_ids) + for p in posts: + page_info[p.id] = {"title": p.title, "slug": p.slug} + + return entries, has_more, pending_tickets, page_info + + @bp.get("/") + async def index(): + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets, page_info = await _load_entries(page) + + ctx = dict( + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page_info=page_info, + page=page, + view=view, + ) + + if is_htmx_request(): + html = await render_template("_types/all_events/_main_panel.html", **ctx) + else: + html = await render_template("_types/all_events/index.html", **ctx) + + return await make_response(html, 200) + + @bp.get("/all-entries") + async def entries_fragment(): + view = request.args.get("view", "list") + page = int(request.args.get("page", 1)) + + entries, has_more, pending_tickets, page_info = await _load_entries(page) + + html = await render_template( + "_types/all_events/_cards.html", + entries=entries, + has_more=has_more, + pending_tickets=pending_tickets, + page_info=page_info, + page=page, + view=view, + ) + return await make_response(html, 200) + + @bp.post("/all-tickets/adjust") + async def adjust_ticket(): + """Adjust ticket quantity, return updated widget + OOB cart-mini.""" + ident = current_cart_identity() + form = await request.form + entry_id = int(form.get("entry_id", 0)) + count = max(int(form.get("count", 0)), 0) + tt_raw = (form.get("ticket_type_id") or "").strip() + ticket_type_id = int(tt_raw) if tt_raw else None + + await services.calendar.adjust_ticket_quantity( + g.s, entry_id, count, + user_id=ident["user_id"], + session_id=ident["session_id"], + ticket_type_id=ticket_type_id, + ) + + # Get updated ticket count for this entry + tickets = await services.calendar.pending_tickets( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + qty = sum(1 for t in tickets if t.entry_id == entry_id) + + # Load entry DTO for the widget template + entry = await services.calendar.entry_by_id(g.s, entry_id) + + # Updated cart count for OOB mini-cart + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + cart_count = summary.count + summary.calendar_count + summary.ticket_count + + # Render widget + OOB cart-mini + widget_html = await render_template( + "_types/page_summary/_ticket_widget.html", + entry=entry, + qty=qty, + ticket_url="/all-tickets/adjust", + ) + mini_html = await render_template_string( + '{% from "_types/cart/_mini.html" import mini with context %}' + '{{ mini(oob="true") }}', + cart_count=cart_count, + ) + return await make_response(widget_html + mini_html, 200) + + return bp diff --git a/bp/page/routes.py b/bp/page/routes.py index 5be7143..da4fb74 100644 --- a/bp/page/routes.py +++ b/bp/page/routes.py @@ -1,9 +1,8 @@ """ -Page summary blueprint — shows upcoming events across all calendars -for all pages. +Page summary blueprint — shows upcoming events for a single page's calendars. Routes: - GET // — full page with first page of entries + GET // — full page scoped to this page GET //entries — HTMX fragment for infinite scroll POST //tickets/adjust — adjust ticket quantity inline """ @@ -19,10 +18,10 @@ from shared.services.registry import services def register() -> Blueprint: bp = Blueprint("page_summary", __name__) - async def _load_entries(page, per_page=20): - """Load all upcoming entries + pending ticket counts + page titles.""" + async def _load_entries(post_id, page, per_page=20): + """Load upcoming entries for this page + pending ticket counts.""" entries, has_more = await services.calendar.upcoming_entries_for_container( - g.s, page=page, per_page=per_page, + g.s, "page", post_id, page=page, per_page=per_page, ) # Pending ticket counts keyed by entry_id @@ -36,33 +35,21 @@ def register() -> Blueprint: if t.entry_id is not None: pending_tickets[t.entry_id] = pending_tickets.get(t.entry_id, 0) + 1 - # Batch-load page info for container_ids - page_info = {} # {post_id: {title, slug}} - if entries: - post_ids = list({ - e.calendar_container_id - for e in entries - if e.calendar_container_type == "page" and e.calendar_container_id - }) - if post_ids: - posts = await services.blog.get_posts_by_ids(g.s, post_ids) - for p in posts: - page_info[p.id] = {"title": p.title, "slug": p.slug} - - return entries, has_more, pending_tickets, page_info + 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, page_info = await _load_entries(page) + entries, has_more, pending_tickets = await _load_entries(post["id"], page) ctx = dict( entries=entries, has_more=has_more, pending_tickets=pending_tickets, - page_info=page_info, + page_info={}, page=page, view=view, ) @@ -76,17 +63,18 @@ def register() -> Blueprint: @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, page_info = await _load_entries(page) + 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_info=page_info, + page_info={}, page=page, view=view, ) diff --git a/templates/_types/all_events/_card.html b/templates/_types/all_events/_card.html new file mode 100644 index 0000000..0005563 --- /dev/null +++ b/templates/_types/all_events/_card.html @@ -0,0 +1,62 @@ +{# List card for all events — one entry #} +{% set pi = page_info.get(entry.calendar_container_id, {}) %} +{% set page_slug = pi.get('slug', '') %} +{% set page_title = pi.get('title') %} +
+
+ {# Left: event info #} +
+ {% if page_slug %} + {% set day_href = events_url('/' ~ page_slug ~ '/calendars/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %} + {% else %} + {% set day_href = '' %} + {% endif %} + {% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' if day_href else '' %} + {% if entry_href %} + +

{{ entry.name }}

+
+ {% else %} +

{{ entry.name }}

+ {% endif %} + +
+ {% if page_title %} + + {{ page_title }} + + {% endif %} + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} +
+ +
+ {% if day_href %} + {{ entry.start_at.strftime('%a %-d %b') }} · + {% else %} + {{ entry.start_at.strftime('%a %-d %b') }} · + {% 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 = url_for('all_events.adjust_ticket') %} + {% include '_types/page_summary/_ticket_widget.html' %} +
+ {% endif %} +
+
diff --git a/templates/_types/all_events/_card_tile.html b/templates/_types/all_events/_card_tile.html new file mode 100644 index 0000000..3f8855f --- /dev/null +++ b/templates/_types/all_events/_card_tile.html @@ -0,0 +1,60 @@ +{# Tile card for all events — compact event tile #} +{% set pi = page_info.get(entry.calendar_container_id, {}) %} +{% set page_slug = pi.get('slug', '') %} +{% set page_title = pi.get('title') %} +
+ {% if page_slug %} + {% set day_href = events_url('/' ~ page_slug ~ '/calendars/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %} + {% else %} + {% set day_href = '' %} + {% endif %} + {% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' if day_href else '' %} +
+ {% if entry_href %} + +

{{ entry.name }}

+
+ {% else %} +

{{ entry.name }}

+ {% endif %} + +
+ {% if page_title %} + + {{ page_title }} + + {% endif %} + {% if entry.calendar_name %} + + {{ entry.calendar_name }} + + {% endif %} +
+ +
+ {% if day_href %} + {{ entry.start_at.strftime('%a %-d %b') }} + {% else %} + {{ entry.start_at.strftime('%a %-d %b') }} + {% 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 %} +
+ + {# Ticket widget below card #} + {% if entry.ticket_price is not none %} +
+ {% set qty = pending_tickets.get(entry.id, 0) %} + {% set ticket_url = url_for('all_events.adjust_ticket') %} + {% include '_types/page_summary/_ticket_widget.html' %} +
+ {% endif %} +
diff --git a/templates/_types/all_events/_cards.html b/templates/_types/all_events/_cards.html new file mode 100644 index 0000000..0e3c6b8 --- /dev/null +++ b/templates/_types/all_events/_cards.html @@ -0,0 +1,31 @@ +{% for entry in entries %} + {% if view == 'tile' %} + {% include "_types/all_events/_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/all_events/_card.html" %} + {% endif %} +{% endfor %} +{% if has_more %} + {# Infinite scroll sentinel #} + {% set entries_url = url_for('all_events.entries_fragment', page=page + 1, view=view if view != 'list' else '')|host %} + +{% endif %} diff --git a/templates/_types/all_events/_main_panel.html b/templates/_types/all_events/_main_panel.html new file mode 100644 index 0000000..0130973 --- /dev/null +++ b/templates/_types/all_events/_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/all_events/_cards.html" %} +
+ {% else %} +
+ {% include "_types/all_events/_cards.html" %} +
+ {% endif %} +{% else %} +
+ +

No upcoming events

+
+{% endif %} +
diff --git a/templates/_types/all_events/index.html b/templates/_types/all_events/index.html new file mode 100644 index 0000000..00a9696 --- /dev/null +++ b/templates/_types/all_events/index.html @@ -0,0 +1,7 @@ +{% extends '_types/root/_index.html' %} + +{% block meta %}{% endblock %} + +{% block content %} + {% include '_types/all_events/_main_panel.html' %} +{% endblock %}