Inline ticket +/- updates without full page refresh
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 58s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 58s
Extract shared _ticket_widget.html with stable #page-ticket-{id} target.
Adjust route returns re-rendered widget + OOB cart-mini swap, same
pattern as the entry detail page's ticket adjust.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ Routes:
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, request, render_template, make_response
|
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.browser.app.utils.htmx import is_htmx_request
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
@@ -81,7 +81,7 @@ def register() -> Blueprint:
|
|||||||
|
|
||||||
@bp.post("/tickets/adjust")
|
@bp.post("/tickets/adjust")
|
||||||
async def adjust_ticket():
|
async def adjust_ticket():
|
||||||
"""Adjust ticket quantity and refresh the page."""
|
"""Adjust ticket quantity, return updated widget + OOB cart-mini."""
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
form = await request.form
|
form = await request.form
|
||||||
entry_id = int(form.get("entry_id", 0))
|
entry_id = int(form.get("entry_id", 0))
|
||||||
@@ -96,8 +96,33 @@ def register() -> Blueprint:
|
|||||||
ticket_type_id=ticket_type_id,
|
ticket_type_id=ticket_type_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
resp = await make_response("", 200)
|
# Get updated ticket count for this entry
|
||||||
resp.headers["HX-Refresh"] = "true"
|
tickets = await services.calendar.pending_tickets(
|
||||||
return resp
|
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=f"/{g.post_slug}/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
|
return bp
|
||||||
|
|||||||
@@ -31,63 +31,7 @@
|
|||||||
<div class="shrink-0">
|
<div class="shrink-0">
|
||||||
{% set qty = pending_tickets.get(entry.id, 0) %}
|
{% set qty = pending_tickets.get(entry.id, 0) %}
|
||||||
{% set ticket_url = url_for('page_summary.adjust_ticket') %}
|
{% set ticket_url = url_for('page_summary.adjust_ticket') %}
|
||||||
<div class="flex items-center gap-2 text-sm">
|
{% include '_types/page_summary/_ticket_widget.html' %}
|
||||||
<span class="text-green-600 font-medium">£{{ '%.2f'|format(entry.ticket_price) }}</span>
|
|
||||||
|
|
||||||
{% if qty == 0 %}
|
|
||||||
<form
|
|
||||||
action="{{ ticket_url }}"
|
|
||||||
method="post"
|
|
||||||
hx-post="{{ ticket_url }}"
|
|
||||||
hx-swap="none"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
|
||||||
<input type="hidden" name="count" value="1">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1"
|
|
||||||
>
|
|
||||||
<i class="fa fa-cart-plus text-2xl" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<form
|
|
||||||
action="{{ ticket_url }}"
|
|
||||||
method="post"
|
|
||||||
hx-post="{{ ticket_url }}"
|
|
||||||
hx-swap="none"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
|
||||||
<input type="hidden" name="count" value="{{ qty - 1 }}">
|
|
||||||
<button type="submit"
|
|
||||||
class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl">-</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<a class="relative inline-flex items-center justify-center text-emerald-700" href="{{ cart_url('/') }}">
|
|
||||||
<span class="relative inline-flex items-center justify-center">
|
|
||||||
<i class="fa-solid fa-shopping-cart text-xl" aria-hidden="true"></i>
|
|
||||||
<span class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none">
|
|
||||||
<span class="flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold">{{ qty }}</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<form
|
|
||||||
action="{{ ticket_url }}"
|
|
||||||
method="post"
|
|
||||||
hx-post="{{ ticket_url }}"
|
|
||||||
hx-swap="none"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
|
||||||
<input type="hidden" name="count" value="{{ qty + 1 }}">
|
|
||||||
<button type="submit"
|
|
||||||
class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl">+</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,58 +28,10 @@
|
|||||||
|
|
||||||
{# Ticket widget below card #}
|
{# Ticket widget below card #}
|
||||||
{% if entry.ticket_price is not none %}
|
{% if entry.ticket_price is not none %}
|
||||||
<div class="border-t border-stone-100 px-3 py-2 flex items-center justify-between">
|
<div class="border-t border-stone-100 px-3 py-2">
|
||||||
<span class="text-xs text-green-600 font-medium">£{{ '%.2f'|format(entry.ticket_price) }}/ticket</span>
|
|
||||||
|
|
||||||
{% set qty = pending_tickets.get(entry.id, 0) %}
|
{% set qty = pending_tickets.get(entry.id, 0) %}
|
||||||
{% set ticket_url = url_for('page_summary.adjust_ticket') %}
|
{% set ticket_url = url_for('page_summary.adjust_ticket') %}
|
||||||
|
{% include '_types/page_summary/_ticket_widget.html' %}
|
||||||
{% if qty == 0 %}
|
|
||||||
<form
|
|
||||||
action="{{ ticket_url }}"
|
|
||||||
method="post"
|
|
||||||
hx-post="{{ ticket_url }}"
|
|
||||||
hx-swap="none"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
|
||||||
<input type="hidden" name="count" value="1">
|
|
||||||
<button type="submit"
|
|
||||||
class="relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1">
|
|
||||||
<i class="fa fa-cart-plus text-xl" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{% else %}
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<form
|
|
||||||
action="{{ ticket_url }}"
|
|
||||||
method="post"
|
|
||||||
hx-post="{{ ticket_url }}"
|
|
||||||
hx-swap="none"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
|
||||||
<input type="hidden" name="count" value="{{ qty - 1 }}">
|
|
||||||
<button type="submit"
|
|
||||||
class="inline-flex items-center justify-center w-6 h-6 text-xs font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50">-</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<span class="inline-flex items-center justify-center px-1.5 py-0.5 rounded-full bg-stone-100 text-xs font-medium">{{ qty }}</span>
|
|
||||||
|
|
||||||
<form
|
|
||||||
action="{{ ticket_url }}"
|
|
||||||
method="post"
|
|
||||||
hx-post="{{ ticket_url }}"
|
|
||||||
hx-swap="none"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
|
||||||
<input type="hidden" name="count" value="{{ qty + 1 }}">
|
|
||||||
<button type="submit"
|
|
||||||
class="inline-flex items-center justify-center w-6 h-6 text-xs font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50">+</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
63
templates/_types/page_summary/_ticket_widget.html
Normal file
63
templates/_types/page_summary/_ticket_widget.html
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{# Inline ticket +/- widget for page summary cards.
|
||||||
|
Variables: entry, qty, ticket_url
|
||||||
|
Wrapped in a div with stable ID for HTMX targeting. #}
|
||||||
|
<div id="page-ticket-{{ entry.id }}" class="flex items-center gap-2">
|
||||||
|
<span class="text-green-600 font-medium text-sm">£{{ '%.2f'|format(entry.ticket_price) }}</span>
|
||||||
|
|
||||||
|
{% if qty == 0 %}
|
||||||
|
<form
|
||||||
|
action="{{ ticket_url }}"
|
||||||
|
method="post"
|
||||||
|
hx-post="{{ ticket_url }}"
|
||||||
|
hx-target="#page-ticket-{{ entry.id }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
||||||
|
<input type="hidden" name="count" value="1">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1"
|
||||||
|
>
|
||||||
|
<i class="fa fa-cart-plus text-2xl" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<form
|
||||||
|
action="{{ ticket_url }}"
|
||||||
|
method="post"
|
||||||
|
hx-post="{{ ticket_url }}"
|
||||||
|
hx-target="#page-ticket-{{ entry.id }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
||||||
|
<input type="hidden" name="count" value="{{ qty - 1 }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl">-</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<a class="relative inline-flex items-center justify-center text-emerald-700" href="{{ cart_url('/') }}">
|
||||||
|
<span class="relative inline-flex items-center justify-center">
|
||||||
|
<i class="fa-solid fa-shopping-cart text-xl" aria-hidden="true"></i>
|
||||||
|
<span class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none">
|
||||||
|
<span class="flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold">{{ qty }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form
|
||||||
|
action="{{ ticket_url }}"
|
||||||
|
method="post"
|
||||||
|
hx-post="{{ ticket_url }}"
|
||||||
|
hx-target="#page-ticket-{{ entry.id }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
||||||
|
<input type="hidden" name="count" value="{{ qty + 1 }}">
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl">+</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user