Convert events header/panel f-string HTML to sexp calls

Migrates ~20 functions from f-string HTML construction to sexp():
- _oob_header_html, _post_header_html label/cart badge
- _calendars_header_html, _calendar_header_html, _calendar_nav_html
- _day_header_html, _day_nav_html (entries scroll menu + admin cog)
- _markets_header_html, _payments_header_html labels
- _calendars_main_panel_html + _calendars_list_html
- _calendar_main_panel_html (full month grid with day cells + entry badges)
- _day_main_panel_html + _day_row_html (entries table)
- _calendar_admin_main_panel_html + _calendar_description_display_html
- _markets_main_panel_html + _markets_list_html
- _payments_main_panel_html (SumUp config form)
- _entry_state_badge_html, _ticket_state_badge_html
- _day_admin_main_panel_html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 12:19:57 +00:00
parent eda95ec58b
commit 903193d825

View File

@@ -24,10 +24,12 @@ from shared.sexp.helpers import (
def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str: def _oob_header_html(parent_id: str, child_id: str, row_html: str) -> str:
"""Wrap a header row in OOB div with child placeholder.""" """Wrap a header row in OOB div with child placeholder."""
return ( return sexp(
f'<div id="{parent_id}" hx-swap-oob="outerHTML" class="w-full">' '(div :id pid :hx-swap-oob "outerHTML" :class "w-full"'
f'<div class="w-full">{row_html}' ' (div :class "w-full"'
f'<div id="{child_id}"></div></div></div>' ' (raw! rh)'
' (div :id cid)))',
pid=parent_id, cid=child_id, rh=row_html,
) )
@@ -42,24 +44,23 @@ def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
title = (post.get("title") or "")[:160] title = (post.get("title") or "")[:160]
feature_image = post.get("feature_image") feature_image = post.get("feature_image")
label_parts = [] label_html = sexp(
if feature_image: '(<> (when fi (img :src fi :class "h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"))'
label_parts.append( ' (span t))',
f'<img src="{feature_image}" class="h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0">' fi=feature_image, t=title,
) )
label_parts.append(f"<span>{escape(title)}</span>")
label_html = "".join(label_parts)
nav_parts = [] nav_parts = []
page_cart_count = ctx.get("page_cart_count", 0) page_cart_count = ctx.get("page_cart_count", 0)
if page_cart_count and page_cart_count > 0: if page_cart_count and page_cart_count > 0:
cart_href = call_url(ctx, "cart_url", f"/{slug}/") cart_href = call_url(ctx, "cart_url", f"/{slug}/")
nav_parts.append( nav_parts.append(sexp(
f'<a href="{cart_href}" class="relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full' '(a :href ch :class "relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full'
f' border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition">' ' border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"'
f'<i class="fa fa-shopping-cart" aria-hidden="true"></i>' ' (i :class "fa fa-shopping-cart" :aria-hidden "true")'
f'<span>{page_cart_count}</span></a>' ' (span cnt))',
) ch=cart_href, cnt=str(page_cart_count),
))
# Post nav: calendar links + admin # Post nav: calendar links + admin
nav_parts.append(_post_nav_html(ctx)) nav_parts.append(_post_nav_html(ctx))
@@ -124,7 +125,7 @@ def _calendars_header_html(ctx: dict, *, oob: bool = False) -> str:
' :link-href lh :link-label-html llh' ' :link-href lh :link-label-html llh'
' :child-id "calendars-header-child" :oob oob)', ' :child-id "calendars-header-child" :oob oob)',
lh=link_href, lh=link_href,
llh='<i class="fa fa-calendar" aria-hidden="true"></i><div>Calendars</div>', llh=sexp('(<> (i :class "fa fa-calendar" :aria-hidden "true") (div "Calendars"))'),
oob=oob, oob=oob,
) )
@@ -144,14 +145,15 @@ def _calendar_header_html(ctx: dict, *, oob: bool = False) -> str:
cal_desc = getattr(calendar, "description", "") or "" cal_desc = getattr(calendar, "description", "") or ""
link_href = url_for("calendars.calendar.get", calendar_slug=cal_slug) link_href = url_for("calendars.calendar.get", calendar_slug=cal_slug)
label_html = ( label_html = sexp(
'<div class="flex flex-col md:flex-row md:gap-2 items-center min-w-0">' '(div :class "flex flex-col md:flex-row md:gap-2 items-center min-w-0"'
'<div class="flex flex-row items-center gap-2">' ' (div :class "flex flex-row items-center gap-2"'
f'<i class="fa fa-calendar"></i>' ' (i :class "fa fa-calendar")'
f'<div class="shrink-0">{escape(cal_name)}</div>' ' (div :class "shrink-0" n))'
'</div>' ' (div :id "calendar-description-title"'
f'<div id="calendar-description-title" class="text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block">{escape(cal_desc)}</div>' ' :class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"'
'</div>' ' d))',
n=cal_name, d=cal_desc,
) )
# Desktop nav: slots + admin # Desktop nav: slots + admin
@@ -188,10 +190,10 @@ def _calendar_nav_html(ctx: dict) -> str:
)) ))
if is_admin: if is_admin:
admin_href = url_for("calendars.calendar.admin.admin", calendar_slug=cal_slug) admin_href = url_for("calendars.calendar.admin.admin", calendar_slug=cal_slug)
parts.append( parts.append(sexp(
f'<a href="{admin_href}" class="flex items-center gap-2 px-3 py-2 rounded text-sm">' '(~nav-link :href h :icon "fa fa-cog" :select-colours sc)',
f'<i class="fa fa-cog" aria-hidden="true"></i></a>' h=admin_href, sc=select_colours,
) ))
return "".join(parts) return "".join(parts)
@@ -217,11 +219,11 @@ def _day_header_html(ctx: dict, *, oob: bool = False) -> str:
month=day_date.month, month=day_date.month,
day=day_date.day, day=day_date.day,
) )
label_html = ( label_html = sexp(
'<div class="flex gap-1 items-center">' '(div :class "flex gap-1 items-center"'
f'<i class="fa fa-calendar-day"></i>' ' (i :class "fa fa-calendar-day")'
f' {escape(day_date.strftime("%A %d %B %Y"))}' ' (span d))',
'</div>' d=day_date.strftime("%A %d %B %Y"),
) )
nav_html = _day_nav_html(ctx) nav_html = _day_nav_html(ctx)
@@ -252,11 +254,7 @@ def _day_nav_html(ctx: dict) -> str:
parts = [] parts = []
# Confirmed entries nav (scrolling menu) # Confirmed entries nav (scrolling menu)
if confirmed_entries: if confirmed_entries:
parts.append( entry_links = []
'<div class="flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"'
' id="day-entries-nav-wrapper">'
'<div class="flex overflow-x-auto gap-1 scrollbar-thin">'
)
for entry in confirmed_entries: for entry in confirmed_entries:
href = url_for( href = url_for(
"calendars.calendar.day.calendar_entries.calendar_entry.get", "calendars.calendar.day.calendar_entries.calendar_entry.get",
@@ -266,17 +264,23 @@ def _day_nav_html(ctx: dict) -> str:
day=day_date.day, day=day_date.day,
entry_id=entry.id, entry_id=entry.id,
) )
name = escape(entry.name)
start = entry.start_at.strftime("%H:%M") if entry.start_at else "" start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
end = f" {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" end = f" {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
parts.append( entry_links.append(sexp(
f'<a href="{href}" class="flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0">' '(a :href h :class "flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0"'
f'<div class="flex-1 min-w-0">' ' (div :class "flex-1 min-w-0"'
f'<div class="font-medium truncate">{name}</div>' ' (div :class "font-medium truncate" n)'
f'<div class="text-xs text-stone-600 truncate">{start}{end}</div>' ' (div :class "text-xs text-stone-600 truncate" t)))',
f'</div></a>' h=href, n=entry.name, t=f"{start}{end}",
) ))
parts.append('</div></div>') inner = "".join(entry_links)
parts.append(sexp(
'(div :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"'
' :id "day-entries-nav-wrapper"'
' (div :class "flex overflow-x-auto gap-1 scrollbar-thin"'
' (raw! inner)))',
inner=inner,
))
if is_admin and day_date: if is_admin and day_date:
admin_href = url_for( admin_href = url_for(
@@ -286,10 +290,10 @@ def _day_nav_html(ctx: dict) -> str:
month=day_date.month, month=day_date.month,
day=day_date.day, day=day_date.day,
) )
parts.append( parts.append(sexp(
f'<a href="{admin_href}" class="flex items-center gap-2 px-3 py-2 rounded text-sm">' '(~nav-link :href h :icon "fa fa-cog")',
f'<i class="fa fa-cog" aria-hidden="true"></i></a>' h=admin_href,
) ))
return "".join(parts) return "".join(parts)
@@ -370,7 +374,7 @@ def _markets_header_html(ctx: dict, *, oob: bool = False) -> str:
' :link-href lh :link-label-html llh' ' :link-href lh :link-label-html llh'
' :child-id "markets-header-child" :oob oob)', ' :child-id "markets-header-child" :oob oob)',
lh=link_href, lh=link_href,
llh='<i class="fa fa-shopping-bag" aria-hidden="true"></i><div>Markets</div>', llh=sexp('(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div "Markets"))'),
oob=oob, oob=oob,
) )
@@ -388,7 +392,7 @@ def _payments_header_html(ctx: dict, *, oob: bool = False) -> str:
' :link-href lh :link-label-html llh' ' :link-href lh :link-label-html llh'
' :child-id "payments-header-child" :oob oob)', ' :child-id "payments-header-child" :oob oob)',
lh=link_href, lh=link_href,
llh='<i class="fa fa-credit-card" aria-hidden="true"></i><div>Payments</div>', llh=sexp('(<> (i :class "fa fa-credit-card" :aria-hidden "true") (div "Payments"))'),
oob=oob, oob=oob,
) )
@@ -408,41 +412,45 @@ def _calendars_main_panel_html(ctx: dict) -> str:
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
calendars = ctx.get("calendars") or [] calendars = ctx.get("calendars") or []
hx_select = ctx.get("hx_select_search", "#main-panel")
parts = ['<section class="p-4">'] form_html = ""
if can_create: if can_create:
create_url = url_for("calendars.create_calendar") create_url = url_for("calendars.create_calendar")
parts.append( form_html = sexp(
'<div id="cal-create-errors" class="mt-2 text-sm text-red-600"></div>' '(<>'
f'<form class="mt-4 flex gap-2 items-end" hx-post="{create_url}" ' ' (div :id "cal-create-errors" :class "mt-2 text-sm text-red-600")'
'hx-target="#calendars-list" hx-select="#calendars-list" hx-swap="outerHTML" ' ' (form :class "mt-4 flex gap-2 items-end" :hx-post cu'
"""hx-on::before-request="document.querySelector('#cal-create-errors').textContent='';" """ ' :hx-target "#calendars-list" :hx-select "#calendars-list" :hx-swap "outerHTML"'
"""hx-on::response-error="document.querySelector('#cal-create-errors').innerHTML = event.detail.xhr.responseText;">""" """ :hx-on::before-request "document.querySelector('#cal-create-errors').textContent='';" """
f'<input type="hidden" name="csrf_token" value="{csrf}">' """ :hx-on::response-error "document.querySelector('#cal-create-errors').innerHTML = event.detail.xhr.responseText;" """
'<div class="flex-1"><label class="block text-sm text-gray-600">Name</label>' ' (input :type "hidden" :name "csrf_token" :value csrf)'
'<input name="name" type="text" required class="w-full border rounded px-3 py-2" ' ' (div :class "flex-1"'
'placeholder="e.g. Events, Gigs, Meetings" /></div>' ' (label :class "block text-sm text-gray-600" "Name")'
'<button type="submit" class="border rounded px-3 py-2">Add calendar</button></form>' ' (input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"'
' :placeholder "e.g. Events, Gigs, Meetings"))'
' (button :type "submit" :class "border rounded px-3 py-2" "Add calendar")))',
cu=create_url, csrf=csrf,
) )
parts.append('<div id="calendars-list" class="mt-6">') list_html = _calendars_list_html(ctx, calendars)
parts.append(_calendars_list_html(ctx, calendars)) return sexp(
parts.append('</div></section>') '(section :class "p-4"'
return "".join(parts) ' (raw! fh)'
' (div :id "calendars-list" :class "mt-6" (raw! lh)))',
fh=form_html, lh=list_html,
)
def _calendars_list_html(ctx: dict, calendars: list) -> str: def _calendars_list_html(ctx: dict, calendars: list) -> str:
"""Render the calendars list items.""" """Render the calendars list items."""
from quart import url_for from quart import url_for
from shared.utils import route_prefix from shared.utils import route_prefix
hx_select = ctx.get("hx_select_search", "#main-panel")
csrf_token = ctx.get("csrf_token") csrf_token = ctx.get("csrf_token")
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
prefix = route_prefix() prefix = route_prefix()
if not calendars: if not calendars:
return '<p class="text-gray-500 mt-4">No calendars yet. Create one above.</p>' return sexp('(p :class "text-gray-500 mt-4" "No calendars yet. Create one above.")')
parts = [] parts = []
for cal in calendars: for cal in calendars:
@@ -450,22 +458,25 @@ def _calendars_list_html(ctx: dict, calendars: list) -> str:
cal_name = getattr(cal, "name", "") cal_name = getattr(cal, "name", "")
href = prefix + url_for("calendars.calendar.get", calendar_slug=cal_slug) href = prefix + url_for("calendars.calendar.get", calendar_slug=cal_slug)
del_url = url_for("calendars.calendar.delete", calendar_slug=cal_slug) del_url = url_for("calendars.calendar.delete", calendar_slug=cal_slug)
parts.append( csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
f'<div class="mt-6 border rounded-lg p-4"><div class="flex items-center justify-between gap-3">' parts.append(sexp(
f'<a class="flex items-baseline gap-3" href="{href}" ' '(div :class "mt-6 border rounded-lg p-4"'
f'hx-get="{href}" hx-target="#main-panel" hx-select="{hx_select}" hx-swap="outerHTML" hx-push-url="true">' ' (div :class "flex items-center justify-between gap-3"'
f'<h3 class="font-semibold">{escape(cal_name)}</h3>' ' (a :class "flex items-baseline gap-3" :href h'
f'<h4 class="text-gray-500">/{escape(cal_slug)}/</h4></a>' ' :hx-get h :hx-target "#main-panel" :hx-select "#main-panel" :hx-swap "outerHTML" :hx-push-url "true"'
f'<button class="text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400" ' ' (h3 :class "font-semibold" cn)'
f'data-confirm data-confirm-title="Delete calendar?" ' ' (h4 :class "text-gray-500" (str "/" cs "/")))'
f'data-confirm-text="Entries will be hidden (soft delete)" ' ' (button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"'
f'data-confirm-icon="warning" data-confirm-confirm-text="Yes, delete it" ' ' :data-confirm true :data-confirm-title "Delete calendar?"'
f'data-confirm-cancel-text="Cancel" data-confirm-event="confirmed" ' ' :data-confirm-text "Entries will be hidden (soft delete)"'
f'hx-delete="{del_url}" hx-trigger="confirmed" ' ' :data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"'
f'hx-target="#calendars-list" hx-select="#calendars-list" hx-swap="outerHTML" ' ' :data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"'
f"""hx-headers='{{"X-CSRFToken":"{csrf}"}}'>""" ' :hx-delete du :hx-trigger "confirmed"'
f'<i class="fa-solid fa-trash"></i></button></div></div>' ' :hx-target "#calendars-list" :hx-select "#calendars-list" :hx-swap "outerHTML"'
) ' :hx-headers ch'
' (i :class "fa-solid fa-trash"))))',
h=href, cn=cal_name, cs=cal_slug, du=del_url, ch=csrf_hdr,
))
return "".join(parts) return "".join(parts)
@@ -502,50 +513,39 @@ def _calendar_main_panel_html(ctx: dict) -> str:
qs = qsession if "qsession" not in ctx else ctx["qsession"] qs = qsession if "qsession" not in ctx else ctx["qsession"]
def nav_link(y, m): def nav_link(y, m):
href = url_for("calendars.calendar.get", calendar_slug=cal_slug, year=y, month=m) return url_for("calendars.calendar.get", calendar_slug=cal_slug, year=y, month=m)
return href
# Month navigation header # Month navigation arrows
parts = ['<section class="bg-orange-100">'] nav_arrows = []
parts.append('<header class="flex items-center justify-center mt-2">')
parts.append('<nav class="flex items-center gap-2 text-2xl">')
# Year/month nav arrows
for label, yr, mn in [ for label, yr, mn in [
("&laquo;", prev_year, month), ("\u00ab", prev_year, month),
("&lsaquo;", prev_month_year, prev_month), ("\u2039", prev_month_year, prev_month),
]: ]:
href = nav_link(yr, mn) href = nav_link(yr, mn)
parts.append( nav_arrows.append(sexp(
f'<a class="{pill_cls} text-xl" href="{href}" ' '(a :class (str pc " text-xl") :href h'
f'hx-get="{href}" hx-target="#main-panel" hx-select="{hx_select}" ' ' :hx-get h :hx-target "#main-panel" :hx-select "#main-panel" :hx-swap "outerHTML" :hx-push-url "true" l)',
f'hx-swap="outerHTML" hx-push-url="true">{label}</a>' pc=pill_cls, h=href, l=label,
) ))
parts.append(f'<div class="px-3 font-medium">{escape(month_name)} {year}</div>') nav_arrows.append(sexp('(div :class "px-3 font-medium" (str mn " " yr))', mn=month_name, yr=str(year)))
for label, yr, mn in [ for label, yr, mn in [
("&rsaquo;", next_month_year, next_month), ("\u203a", next_month_year, next_month),
("&raquo;", next_year, month), ("\u00bb", next_year, month),
]: ]:
href = nav_link(yr, mn) href = nav_link(yr, mn)
parts.append( nav_arrows.append(sexp(
f'<a class="{pill_cls} text-xl" href="{href}" ' '(a :class (str pc " text-xl") :href h'
f'hx-get="{href}" hx-target="#main-panel" hx-select="{hx_select}" ' ' :hx-get h :hx-target "#main-panel" :hx-select "#main-panel" :hx-swap "outerHTML" :hx-push-url "true" l)',
f'hx-swap="outerHTML" hx-push-url="true">{label}</a>' pc=pill_cls, h=href, l=label,
) ))
parts.append('</nav></header>')
# Calendar grid
parts.append('<div class="rounded-2xl border border-stone-200 bg-white/80 p-4">')
# Weekday headers # Weekday headers
parts.append('<div class="hidden sm:grid grid-cols-7 text-center text-md font-semibold text-stone-700 mb-2">') wd_html = "".join(sexp('(div :class "py-1" w)', w=wd) for wd in weekday_names)
for wd in weekday_names:
parts.append(f'<div class="py-1">{wd}</div>')
parts.append('</div>')
# Weeks grid # Day cells
parts.append('<div class="grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden">') cells = []
for week in weeks: for week in weeks:
for day_cell in week: for day_cell in week:
if isinstance(day_cell, dict): if isinstance(day_cell, dict):
@@ -563,29 +563,27 @@ def _calendar_main_panel_html(ctx: dict) -> str:
if is_today: if is_today:
cell_cls += " ring-2 ring-blue-500 z-10 relative" cell_cls += " ring-2 ring-blue-500 z-10 relative"
parts.append(f'<div class="{cell_cls}">') # Day number link
parts.append('<div class="flex justify-between items-center"><div class="flex flex-col">') day_num_html = ""
if day_date: day_short_html = ""
parts.append(f'<span class="sm:hidden text-[16px] text-stone-500">{day_date.strftime("%a")}</span>')
# Clickable day number
if day_date: if day_date:
day_href = url_for( day_href = url_for(
"calendars.calendar.day.show_day", "calendars.calendar.day.show_day",
calendar_slug=cal_slug, calendar_slug=cal_slug,
year=day_date.year, year=day_date.year, month=day_date.month, day=day_date.day,
month=day_date.month,
day=day_date.day,
) )
parts.append( day_short_html = sexp(
f'<a class="{pill_cls}" href="{day_href}" ' '(span :class "sm:hidden text-[16px] text-stone-500" d)',
f'hx-get="{day_href}" hx-target="#main-panel" hx-select="{hx_select}" ' d=day_date.strftime("%a"),
f'hx-swap="outerHTML" hx-push-url="true">{day_date.day}</a>' )
day_num_html = sexp(
'(a :class pc :href h :hx-get h :hx-target "#main-panel" :hx-select "#main-panel"'
' :hx-swap "outerHTML" :hx-push-url "true" n)',
pc=pill_cls, h=day_href, n=str(day_date.day),
) )
parts.append('</div></div>')
# Entries for this day # Entry badges for this day
parts.append('<div class="mt-1 space-y-0.5">') entry_badges = []
if day_date: if day_date:
for e in month_entries: for e in month_entries:
if e.start_at and e.start_at.date() == day_date: if e.start_at and e.start_at.date() == day_date:
@@ -597,18 +595,34 @@ def _calendar_main_panel_html(ctx: dict) -> str:
bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800" bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800"
else: else:
bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700" bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700"
state_label = (e.state or "pending").replace("_", " ") state_label = (e.state or "pending").replace("_", " ")
parts.append( entry_badges.append(sexp(
f'<div class="flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 {bg_cls}">' '(div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bc)'
f'<span class="truncate">{escape(e.name)}</span>' ' (span :class "truncate" n)'
f'<span class="shrink-0 text-[10px] font-semibold uppercase tracking-tight">{state_label}</span>' ' (span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" sl))',
f'</div>' bc=bg_cls, n=e.name, sl=state_label,
) ))
parts.append('</div></div>')
parts.append('</div></div></section>') badges_html = "".join(entry_badges)
return "".join(parts) cells.append(sexp(
'(div :class cc'
' (div :class "flex justify-between items-center"'
' (div :class "flex flex-col" (raw! dsh) (raw! dnh)))'
' (div :class "mt-1 space-y-0.5" (raw! bh)))',
cc=cell_cls, dsh=day_short_html, dnh=day_num_html, bh=badges_html,
))
cells_html = "".join(cells)
arrows_html = "".join(nav_arrows)
return sexp(
'(section :class "bg-orange-100"'
' (header :class "flex items-center justify-center mt-2"'
' (nav :class "flex items-center gap-2 text-2xl" (raw! ah)))'
' (div :class "rounded-2xl border border-stone-200 bg-white/80 p-4"'
' (div :class "hidden sm:grid grid-cols-7 text-center text-md font-semibold text-stone-700 mb-2" (raw! wh))'
' (div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" (raw! ch))))',
ah=arrows_html, wh=wd_html, ch=cells_html,
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -634,40 +648,36 @@ def _day_main_panel_html(ctx: dict) -> str:
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "") tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "") pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
parts = [f'<section id="day-entries" class="{list_container}">'] rows_html = ""
parts.append(
'<table class="w-full text-sm border table-fixed"><thead class="bg-stone-100"><tr>'
'<th class="p-2 text-left w-2/6">Name</th>'
'<th class="text-left p-2 w-1/6">Slot/Time</th>'
'<th class="text-left p-2 w-1/6">State</th>'
'<th class="text-left p-2 w-1/6">Cost</th>'
'<th class="text-left p-2 w-1/6">Tickets</th>'
'<th class="text-left p-2 w-1/6">Actions</th>'
'</tr></thead><tbody>'
)
if day_entries: if day_entries:
for entry in day_entries: rows_html = "".join(_day_row_html(ctx, entry) for entry in day_entries)
parts.append(_day_row_html(ctx, entry))
else: else:
parts.append('<tr><td colspan="6" class="p-3 text-stone-500">No entries yet.</td></tr>') rows_html = sexp('(tr (td :colspan "6" :class "p-3 text-stone-500" "No entries yet."))')
parts.append('</tbody></table>')
# Add entry button
add_url = url_for( add_url = url_for(
"calendars.calendar.day.calendar_entries.add_form", "calendars.calendar.day.calendar_entries.add_form",
calendar_slug=cal_slug, calendar_slug=cal_slug,
day=day, month=month, year=year, day=day, month=month, year=year,
) )
parts.append(
f'<div id="entry-add-container" class="mt-4">' return sexp(
f'<button type="button" class="{pre_action}" ' '(section :id "day-entries" :class lc'
f'hx-get="{add_url}" hx-target="#entry-add-container" hx-swap="innerHTML">' ' (table :class "w-full text-sm border table-fixed"'
f'+ Add entry</button></div>' ' (thead :class "bg-stone-100"'
' (tr'
' (th :class "p-2 text-left w-2/6" "Name")'
' (th :class "text-left p-2 w-1/6" "Slot/Time")'
' (th :class "text-left p-2 w-1/6" "State")'
' (th :class "text-left p-2 w-1/6" "Cost")'
' (th :class "text-left p-2 w-1/6" "Tickets")'
' (th :class "text-left p-2 w-1/6" "Actions")))'
' (tbody (raw! rh)))'
' (div :id "entry-add-container" :class "mt-4"'
' (button :type "button" :class pa'
' :hx-get au :hx-target "#entry-add-container" :hx-swap "innerHTML"'
' "+ Add entry")))',
lc=list_container, rh=rows_html, pa=pre_action, au=add_url,
) )
parts.append('</section>')
return "".join(parts)
def _day_row_html(ctx: dict, entry) -> str: def _day_row_html(ctx: dict, entry) -> str:
@@ -689,11 +699,11 @@ def _day_row_html(ctx: dict, entry) -> str:
) )
# Name # Name
name_html = ( name_html = sexp(
f'<td class="p-2 align-top w-2/6"><div class="font-medium">' '(td :class "p-2 align-top w-2/6" (div :class "font-medium"'
f'<a href="{entry_href}" class="{pill_cls}" ' ' (a :href h :class pc :hx-get h :hx-target "#main-panel" :hx-select "#main-panel"'
f'hx-get="{entry_href}" hx-target="#main-panel" hx-select="{hx_select}" hx-swap="outerHTML" hx-push-url="true">' ' :hx-swap "outerHTML" :hx-push-url "true" n)))',
f'{escape(entry.name)}</a></div></td>' h=entry_href, pc=pill_cls, n=entry.name,
) )
# Slot/Time # Slot/Time
@@ -701,47 +711,55 @@ def _day_row_html(ctx: dict, entry) -> str:
if slot: if slot:
slot_href = url_for("calendars.calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=slot.id) slot_href = url_for("calendars.calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=slot.id)
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else "" time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
time_end = f" {slot.time_end.strftime('%H:%M')}" if slot.time_end else "" time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
slot_html = ( slot_html = sexp(
f'<td class="p-2 align-top w-1/6"><div class="text-xs font-medium">' '(td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium"'
f'<a href="{slot_href}" class="{pill_cls}" ' ' (a :href h :class pc :hx-get h :hx-target "#main-panel" :hx-select "#main-panel"'
f'hx-get="{slot_href}" hx-target="#main-panel" hx-select="{hx_select}" hx-swap="outerHTML" hx-push-url="true">' ' :hx-swap "outerHTML" :hx-push-url "true" sn)'
f'{escape(slot.name)}</a>' ' (span :class "text-stone-600 font-normal" (str "(" ts te ")"))))',
f'<span class="text-stone-600 font-normal">({time_start}{time_end})</span>' h=slot_href, pc=pill_cls, sn=slot.name, ts=time_start, te=time_end,
f'</div></td>'
) )
else: else:
start = entry.start_at.strftime("%H:%M") if entry.start_at else "" start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
end = f" {entry.end_at.strftime('%H:%M')}" if entry.end_at else "" end = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
slot_html = f'<td class="p-2 align-top w-1/6"><div class="text-xs text-stone-600">{start}{end}</div></td>' slot_html = sexp(
'(td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str s e)))',
s=start, e=end,
)
# State # State
state = getattr(entry, "state", "pending") or "pending" state = getattr(entry, "state", "pending") or "pending"
state_html = _entry_state_badge_html(state) state_badge = _entry_state_badge_html(state)
state_td = f'<td class="p-2 align-top w-1/6"><div id="entry-state-{entry.id}">{state_html}</div></td>' state_td = sexp(
'(td :class "p-2 align-top w-1/6" (div :id sid (raw! sb)))',
sid=f"entry-state-{entry.id}", sb=state_badge,
)
# Cost # Cost
cost = getattr(entry, "cost", None) cost = getattr(entry, "cost", None)
cost_str = f"£{cost:.2f}" if cost is not None else "£0.00" cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
cost_td = f'<td class="p-2 align-top w-1/6"><span class="font-medium text-green-600">{cost_str}</span></td>' cost_td = sexp('(td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" c))', c=cost_str)
# Tickets # Tickets
tp = getattr(entry, "ticket_price", None) tp = getattr(entry, "ticket_price", None)
if tp is not None: if tp is not None:
tc = getattr(entry, "ticket_count", None) tc = getattr(entry, "ticket_count", None)
tc_str = f"{tc} tickets" if tc is not None else "Unlimited" tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
tickets_td = ( tickets_td = sexp(
f'<td class="p-2 align-top w-1/6"><div class="text-xs space-y-1">' '(td :class "p-2 align-top w-1/6" (div :class "text-xs space-y-1"'
f'<div class="font-medium text-green-600"{tp:.2f}</div>' ' (div :class "font-medium text-green-600" tp)'
f'<div class="text-stone-600">{tc_str}</div></div></td>' ' (div :class "text-stone-600" tc)))',
tp=f"\u00a3{tp:.2f}", tc=tc_str,
) )
else: else:
tickets_td = '<td class="p-2 align-top w-1/6"><span class="text-xs text-stone-400">No tickets</span></td>' tickets_td = sexp('(td :class "p-2 align-top w-1/6" (span :class "text-xs text-stone-400" "No tickets"))')
# Actions (entry options) - keep simple, just link to entry actions_td = sexp('(td :class "p-2 align-top w-1/6")')
actions_td = f'<td class="p-2 align-top w-1/6"></td>'
return f'<tr class="{tr_cls}">{name_html}{slot_html}{state_td}{cost_td}{tickets_td}{actions_td}</tr>' return sexp(
'(tr :class tc (raw! nh) (raw! sh) (raw! std) (raw! ctd) (raw! ttd) (raw! atd))',
tc=tr_cls, nh=name_html, sh=slot_html, std=state_td, ctd=cost_td, ttd=tickets_td, atd=actions_td,
)
def _entry_state_badge_html(state: str) -> str: def _entry_state_badge_html(state: str) -> str:
@@ -755,7 +773,10 @@ def _entry_state_badge_html(state: str) -> str:
} }
cls = state_classes.get(state, "bg-stone-100 text-stone-700") cls = state_classes.get(state, "bg-stone-100 text-stone-700")
label = state.replace("_", " ").capitalize() label = state.replace("_", " ").capitalize()
return f'<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {cls}">{label}</span>' return sexp(
'(span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " c) l)',
c=cls, l=label,
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -764,7 +785,7 @@ def _entry_state_badge_html(state: str) -> str:
def _day_admin_main_panel_html(ctx: dict) -> str: def _day_admin_main_panel_html(ctx: dict) -> str:
"""Render day admin panel (placeholder nav).""" """Render day admin panel (placeholder nav)."""
return '<div class="p-4 text-sm text-stone-500">Admin options</div>' return sexp('(div :class "p-4 text-sm text-stone-500" "Admin options")')
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -786,42 +807,40 @@ def _calendar_admin_main_panel_html(ctx: dict) -> str:
desc_edit_url = url_for("calendars.calendar.admin.calendar_description_edit", calendar_slug=cal_slug) desc_edit_url = url_for("calendars.calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
description_html = _calendar_description_display_html(calendar, desc_edit_url) description_html = _calendar_description_display_html(calendar, desc_edit_url)
parts = ['<section class="max-w-3xl mx-auto p-4 space-y-10">'] return sexp(
parts.append('<div><h2 class="text-xl font-semibold">Calendar configuration</h2>') '(section :class "max-w-3xl mx-auto p-4 space-y-10"'
parts.append('<div id="cal-put-errors" class="mt-2 text-sm text-red-600"></div>') ' (div'
parts.append(f'<div><label class="block text-sm font-medium text-stone-700">Description</label>') ' (h2 :class "text-xl font-semibold" "Calendar configuration")'
parts.append(description_html) ' (div :id "cal-put-errors" :class "mt-2 text-sm text-red-600")'
parts.append('</div>') ' (div (label :class "block text-sm font-medium text-stone-700" "Description")'
' (raw! dh))'
# Hidden form for direct PUT ' (form :id "calendar-form" :method "post" :hx-target "#main-panel" :hx-select "#main-panel"'
parts.append( """ :hx-on::before-request "document.querySelector('#cal-put-errors').textContent='';" """
f'<form id="calendar-form" method="post" hx-target="#main-panel" hx-select="{hx_select}" ' """ :hx-on::response-error "document.querySelector('#cal-put-errors').innerHTML = event.detail.xhr.responseText;" """
"""hx-on::before-request="document.querySelector('#cal-put-errors').textContent='';" """ """ :hx-on::after-request "if (event.detail.successful) this.reset()" """
"""hx-on::response-error="document.querySelector('#cal-put-errors').innerHTML = event.detail.xhr.responseText;" """ ' :class "hidden space-y-4 mt-4" :autocomplete "off"'
"""hx-on::after-request="if (event.detail.successful) this.reset()" """ ' (input :type "hidden" :name "csrf_token" :value csrf)'
'class="hidden space-y-4 mt-4" autocomplete="off">' ' (div (label :class "block text-sm font-medium text-stone-700" "Description")'
f'<input type="hidden" name="csrf_token" value="{csrf}">' ' (div d)'
'<div><label class="block text-sm font-medium text-stone-700">Description</label>' ' (textarea :name "description" :autocomplete "off" :rows "4" :class "w-full p-2 border rounded" d))'
f'<div>{escape(desc)}</div>' ' (div (button :class "px-3 py-2 rounded bg-stone-800 text-white" "Save"))))'
f'<textarea name="description" autocomplete="off" rows="4" class="w-full p-2 border rounded">{escape(desc)}</textarea>' ' (hr :class "border-stone-200"))',
'</div><div><button class="px-3 py-2 rounded bg-stone-800 text-white">Save</button></div></form>' dh=description_html, csrf=csrf, d=desc,
) )
parts.append('</div><hr class="border-stone-200"></section>')
return "".join(parts)
def _calendar_description_display_html(calendar, edit_url: str) -> str: def _calendar_description_display_html(calendar, edit_url: str) -> str:
"""Render calendar description display with edit button.""" """Render calendar description display with edit button."""
desc = getattr(calendar, "description", "") or "" desc = getattr(calendar, "description", "") or ""
if desc: return sexp(
desc_html = f'<p class="text-stone-700 whitespace-pre-line break-all">{escape(desc)}</p>' '(div :id "calendar-description"'
else: ' (if d'
desc_html = '<p class="text-stone-400 italic">No description yet.</p>' ' (p :class "text-stone-700 whitespace-pre-line break-all" d)'
return ( ' (p :class "text-stone-400 italic" "No description yet."))'
f'<div id="calendar-description">{desc_html}' ' (button :type "button" :class "mt-2 text-xs underline"'
f'<button type="button" class="mt-2 text-xs underline" ' ' :hx-get eu :hx-target "#calendar-description" :hx-swap "outerHTML"'
f'hx-get="{edit_url}" hx-target="#calendar-description" hx-swap="outerHTML">' ' (i :class "fas fa-edit")))',
f'<i class="fas fa-edit"></i></button></div>' d=desc, eu=edit_url,
) )
@@ -839,27 +858,33 @@ def _markets_main_panel_html(ctx: dict) -> str:
csrf_token = ctx.get("csrf_token") csrf_token = ctx.get("csrf_token")
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "") csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
markets = ctx.get("markets") or [] markets = ctx.get("markets") or []
post = ctx.get("post") or {}
parts = ['<section class="p-4">'] form_html = ""
if can_create: if can_create:
create_url = url_for("markets.create_market") create_url = url_for("markets.create_market")
parts.append( form_html = sexp(
'<div id="market-create-errors" class="mt-2 text-sm text-red-600"></div>' '(<>'
f'<form class="mt-4 flex gap-2 items-end" hx-post="{create_url}" ' ' (div :id "market-create-errors" :class "mt-2 text-sm text-red-600")'
'hx-target="#markets-list" hx-select="#markets-list" hx-swap="outerHTML" ' ' (form :class "mt-4 flex gap-2 items-end" :hx-post cu'
"""hx-on::before-request="document.querySelector('#market-create-errors').textContent='';" """ ' :hx-target "#markets-list" :hx-select "#markets-list" :hx-swap "outerHTML"'
"""hx-on::response-error="document.querySelector('#market-create-errors').innerHTML = event.detail.xhr.responseText;">""" """ :hx-on::before-request "document.querySelector('#market-create-errors').textContent='';" """
f'<input type="hidden" name="csrf_token" value="{csrf}">' """ :hx-on::response-error "document.querySelector('#market-create-errors').innerHTML = event.detail.xhr.responseText;" """
'<div class="flex-1"><label class="block text-sm text-gray-600">Name</label>' ' (input :type "hidden" :name "csrf_token" :value csrf)'
'<input name="name" type="text" required class="w-full border rounded px-3 py-2" ' ' (div :class "flex-1"'
'placeholder="e.g. Farm Shop, Bakery" /></div>' ' (label :class "block text-sm text-gray-600" "Name")'
'<button type="submit" class="border rounded px-3 py-2">Add market</button></form>' ' (input :name "name" :type "text" :required true :class "w-full border rounded px-3 py-2"'
' :placeholder "e.g. Farm Shop, Bakery"))'
' (button :type "submit" :class "border rounded px-3 py-2" "Add market")))',
cu=create_url, csrf=csrf,
) )
parts.append('<div id="markets-list" class="mt-6">')
parts.append(_markets_list_html(ctx, markets)) list_html = _markets_list_html(ctx, markets)
parts.append('</div></section>') return sexp(
return "".join(parts) '(section :class "p-4"'
' (raw! fh)'
' (div :id "markets-list" :class "mt-6" (raw! lh)))',
fh=form_html, lh=list_html,
)
def _markets_list_html(ctx: dict, markets: list) -> str: def _markets_list_html(ctx: dict, markets: list) -> str:
@@ -871,7 +896,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
slug = post.get("slug", "") slug = post.get("slug", "")
if not markets: if not markets:
return '<p class="text-gray-500 mt-4">No markets yet. Create one above.</p>' return sexp('(p :class "text-gray-500 mt-4" "No markets yet. Create one above.")')
parts = [] parts = []
for m in markets: for m in markets:
@@ -879,21 +904,24 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "") m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/") market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
del_url = url_for("markets.delete_market", market_slug=m_slug) del_url = url_for("markets.delete_market", market_slug=m_slug)
parts.append( csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
f'<div class="mt-6 border rounded-lg p-4"><div class="flex items-center justify-between gap-3">' parts.append(sexp(
f'<a class="flex items-baseline gap-3" href="{market_href}">' '(div :class "mt-6 border rounded-lg p-4"'
f'<h3 class="font-semibold">{escape(m_name)}</h3>' ' (div :class "flex items-center justify-between gap-3"'
f'<h4 class="text-gray-500">/{escape(m_slug)}/</h4></a>' ' (a :class "flex items-baseline gap-3" :href h'
f'<button class="text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400" ' ' (h3 :class "font-semibold" mn)'
f'data-confirm data-confirm-title="Delete market?" ' ' (h4 :class "text-gray-500" (str "/" ms "/")))'
f'data-confirm-text="Products will be hidden (soft delete)" ' ' (button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"'
f'data-confirm-icon="warning" data-confirm-confirm-text="Yes, delete it" ' ' :data-confirm true :data-confirm-title "Delete market?"'
f'data-confirm-cancel-text="Cancel" data-confirm-event="confirmed" ' ' :data-confirm-text "Products will be hidden (soft delete)"'
f'hx-delete="{del_url}" hx-trigger="confirmed" ' ' :data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"'
f'hx-target="#markets-list" hx-select="#markets-list" hx-swap="outerHTML" ' ' :data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"'
f"""hx-headers='{{"X-CSRFToken":"{csrf}"}}'>""" ' :hx-delete du :hx-trigger "confirmed"'
f'<i class="fa-solid fa-trash"></i></button></div></div>' ' :hx-target "#markets-list" :hx-select "#markets-list" :hx-swap "outerHTML"'
) ' :hx-headers ch'
' (i :class "fa-solid fa-trash"))))',
h=market_href, mn=m_name, ms=m_slug, du=del_url, ch=csrf_hdr,
))
return "".join(parts) return "".join(parts)
@@ -912,30 +940,29 @@ def _payments_main_panel_html(ctx: dict) -> str:
update_url = url_for("payments.update_sumup") update_url = url_for("payments.update_sumup")
placeholder = "--------" if sumup_configured else "sup_sk_..." placeholder = "--------" if sumup_configured else "sup_sk_..."
key_note = '<p class="text-xs text-stone-400 mt-0.5">Key is set. Leave blank to keep current key.</p>' if sumup_configured else "" input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
connected = ('<span class="ml-2 text-xs text-green-600">'
'<i class="fa fa-check-circle"></i> Connected</span>') if sumup_configured else ""
return ( return sexp(
'<section class="p-4 max-w-lg mx-auto">' '(section :class "p-4 max-w-lg mx-auto"'
'<div id="payments-panel" class="space-y-4 p-4 bg-white rounded-lg border border-stone-200">' ' (div :id "payments-panel" :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200"'
'<h3 class="text-lg font-semibold text-stone-800">' ' (h3 :class "text-lg font-semibold text-stone-800"'
'<i class="fa fa-credit-card text-purple-600 mr-1"></i> SumUp Payment</h3>' ' (i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment")'
'<p class="text-xs text-stone-400">Configure per-page SumUp credentials. Leave blank to use the global merchant account.</p>' ' (p :class "text-xs text-stone-400" "Configure per-page SumUp credentials. Leave blank to use the global merchant account.")'
f'<form hx-put="{update_url}" hx-target="#payments-panel" hx-swap="outerHTML" hx-select="#payments-panel" class="space-y-3">' ' (form :hx-put uu :hx-target "#payments-panel" :hx-swap "outerHTML" :hx-select "#payments-panel" :class "space-y-3"'
f'<input type="hidden" name="csrf_token" value="{csrf}">' ' (input :type "hidden" :name "csrf_token" :value csrf)'
'<div><label class="block text-xs font-medium text-stone-600 mb-1">Merchant Code</label>' ' (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Merchant Code")'
f'<input type="text" name="merchant_code" value="{escape(merchant_code)}" placeholder="e.g. ME4J6100" ' ' (input :type "text" :name "merchant_code" :value mc :placeholder "e.g. ME4J6100" :class ic))'
'class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"></div>' ' (div (label :class "block text-xs font-medium text-stone-600 mb-1" "API Key")'
'<div><label class="block text-xs font-medium text-stone-600 mb-1">API Key</label>' ' (input :type "password" :name "api_key" :value "" :placeholder ph :class ic)'
f'<input type="password" name="api_key" value="" placeholder="{placeholder}" ' ' (when sc (p :class "text-xs text-stone-400 mt-0.5" "Key is set. Leave blank to keep current key.")))'
f'class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500">' ' (div (label :class "block text-xs font-medium text-stone-600 mb-1" "Checkout Reference Prefix")'
f'{key_note}</div>' ' (input :type "text" :name "checkout_prefix" :value cp :placeholder "e.g. ROSE-" :class ic))'
'<div><label class="block text-xs font-medium text-stone-600 mb-1">Checkout Reference Prefix</label>' ' (button :type "submit" :class "px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500"'
f'<input type="text" name="checkout_prefix" value="{escape(checkout_prefix)}" placeholder="e.g. ROSE-" ' ' "Save SumUp Settings")'
'class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"></div>' ' (when sc (span :class "ml-2 text-xs text-green-600"'
'<button type="submit" class="px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500">' ' (i :class "fa fa-check-circle") " Connected")))))',
f'Save SumUp Settings</button>{connected}</form></div></section>' uu=update_url, csrf=csrf, mc=merchant_code, ph=placeholder,
ic=input_cls, sc=sumup_configured, cp=checkout_prefix,
) )
@@ -953,7 +980,10 @@ def _ticket_state_badge_html(state: str) -> str:
} }
cls = cls_map.get(state, "bg-stone-100 text-stone-700") cls = cls_map.get(state, "bg-stone-100 text-stone-700")
label = (state or "").replace("_", " ").capitalize() label = (state or "").replace("_", " ").capitalize()
return f'<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {cls}">{label}</span>' return sexp(
'(span :class (str "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium " c) l)',
c=cls, l=label,
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------