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