Send all responses as sexp wire format with client-side rendering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
- Server sends sexp source text, client (sexp.js) renders everything - SexpExpr marker class for nested sexp composition in serialize() - sexp_page() HTML shell with data-mount="body" for full page loads - sexp_response() returns text/sexp for OOB/partial responses - ~app-body layout component replaces ~app-layout (no raw!) - ~rich-text is the only component using raw! (for CMS HTML content) - Fragment endpoints return text/sexp, auto-wrapped in SexpExpr - All _*_html() helpers converted to _*_sexp() returning sexp source - Head auto-hoist: sexp.js moves meta/title/link/script[ld+json] from rendered body to document.head automatically - Unknown components render warning box instead of crashing page - Component kwargs preserve AST for lazy rendering (fixes <> in kwargs) - Fix unterminated paren in events/sexp/tickets.sexpr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ async def events_context() -> dict:
|
||||
"""
|
||||
Events app context processor.
|
||||
|
||||
- nav_tree_html: fetched from blog as fragment
|
||||
- nav_tree: fetched from blog as fragment
|
||||
- cart_count/cart_total: via cart service (shared DB)
|
||||
"""
|
||||
from shared.infrastructure.context import base_context
|
||||
@@ -50,14 +50,14 @@ async def events_context() -> dict:
|
||||
if ident["session_id"] is not None:
|
||||
cart_params["session_id"] = ident["session_id"]
|
||||
|
||||
cart_mini_html, auth_menu_html, nav_tree_html = await fetch_fragments([
|
||||
cart_mini, auth_menu, nav_tree = await fetch_fragments([
|
||||
("cart", "cart-mini", cart_params or None),
|
||||
("account", "auth-menu", {"email": user.email} if user else None),
|
||||
("blog", "nav-tree", {"app_name": "events", "path": request.path}),
|
||||
])
|
||||
ctx["cart_mini_html"] = cart_mini_html
|
||||
ctx["auth_menu_html"] = auth_menu_html
|
||||
ctx["nav_tree_html"] = nav_tree_html
|
||||
ctx["cart_mini"] = cart_mini
|
||||
ctx["auth_menu"] = auth_menu
|
||||
ctx["nav_tree"] = nav_tree
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from __future__ import annotations
|
||||
from quart import Blueprint, g, request, render_template, make_response
|
||||
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||
@@ -70,11 +71,11 @@ def register() -> Blueprint:
|
||||
|
||||
ctx = await get_template_context()
|
||||
if is_htmx_request():
|
||||
html = await render_all_events_oob(ctx, entries, has_more, pending_tickets, page_info, page, view)
|
||||
sexp_src = await render_all_events_oob(ctx, entries, has_more, pending_tickets, page_info, page, view)
|
||||
return sexp_response(sexp_src)
|
||||
else:
|
||||
html = await render_all_events_page(ctx, entries, has_more, pending_tickets, page_info, page, view)
|
||||
|
||||
return await make_response(html, 200)
|
||||
return await make_response(html, 200)
|
||||
|
||||
@bp.get("/all-entries")
|
||||
async def entries_fragment():
|
||||
@@ -84,8 +85,8 @@ def register() -> Blueprint:
|
||||
entries, has_more, pending_tickets, page_info = await _load_entries(page)
|
||||
|
||||
from sexp.sexp_components import render_all_events_cards
|
||||
html = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view)
|
||||
return await make_response(html, 200)
|
||||
sexp_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.post("/all-tickets/adjust")
|
||||
async def adjust_ticket():
|
||||
@@ -127,6 +128,6 @@ def register() -> Blueprint:
|
||||
from sexp.sexp_components import render_ticket_widget
|
||||
widget_html = render_ticket_widget(entry, qty, "/all-tickets/adjust")
|
||||
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
|
||||
return await make_response(widget_html + mini_html, 200)
|
||||
return sexp_response(widget_html + (mini_html or ""))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -7,6 +7,7 @@ from quart import (
|
||||
|
||||
from shared.browser.app.authz import require_admin
|
||||
from shared.browser.app.redis_cacher import clear_cache
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
|
||||
@@ -25,10 +26,10 @@ def register():
|
||||
tctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_calendar_admin_page(tctx)
|
||||
return await make_response(html)
|
||||
else:
|
||||
html = await render_calendar_admin_oob(tctx)
|
||||
|
||||
return await make_response(html)
|
||||
sexp_src = await render_calendar_admin_oob(tctx)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
|
||||
@bp.get("/description/")
|
||||
@@ -36,7 +37,7 @@ def register():
|
||||
async def calendar_description_edit(calendar_slug: str, **kwargs):
|
||||
from sexp.sexp_components import render_calendar_description_edit
|
||||
html = render_calendar_description_edit(g.calendar)
|
||||
return await make_response(html)
|
||||
return sexp_response(html)
|
||||
|
||||
|
||||
@bp.post("/description/")
|
||||
@@ -52,7 +53,7 @@ def register():
|
||||
|
||||
from sexp.sexp_components import render_calendar_description
|
||||
html = render_calendar_description(g.calendar, oob=True)
|
||||
return await make_response(html)
|
||||
return sexp_response(html)
|
||||
|
||||
|
||||
@bp.get("/description/view/")
|
||||
@@ -60,6 +61,6 @@ def register():
|
||||
async def calendar_description_view(calendar_slug: str, **kwargs):
|
||||
from sexp.sexp_components import render_calendar_description
|
||||
html = render_calendar_description(g.calendar)
|
||||
return await make_response(html)
|
||||
return sexp_response(html)
|
||||
|
||||
return bp
|
||||
|
||||
@@ -24,6 +24,7 @@ from .services.calendar_view import (
|
||||
update_calendar_description,
|
||||
)
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
from ..slots.routes import register as register_slots
|
||||
|
||||
@@ -79,12 +80,12 @@ def register():
|
||||
async def inject_root():
|
||||
from shared.infrastructure.fragments import fetch_fragment
|
||||
|
||||
container_nav_html = ""
|
||||
container_nav = ""
|
||||
post_data = getattr(g, "post_data", None)
|
||||
if post_data:
|
||||
post_id = post_data["post"]["id"]
|
||||
post_slug = post_data["post"]["slug"]
|
||||
container_nav_html = await fetch_fragment("relations", "container-nav", params={
|
||||
container_nav = await fetch_fragment("relations", "container-nav", params={
|
||||
"container_type": "page",
|
||||
"container_id": str(post_id),
|
||||
"post_slug": post_slug,
|
||||
@@ -93,7 +94,7 @@ def register():
|
||||
|
||||
return {
|
||||
"calendar": getattr(g, "calendar", None),
|
||||
"container_nav_html": container_nav_html,
|
||||
"container_nav": container_nav,
|
||||
}
|
||||
|
||||
# ---------- Pages ----------
|
||||
@@ -172,10 +173,10 @@ def register():
|
||||
))
|
||||
if not is_htmx_request():
|
||||
html = await render_calendar_page(tctx)
|
||||
return await make_response(html)
|
||||
else:
|
||||
html = await render_calendar_oob(tctx)
|
||||
|
||||
return await make_response(html)
|
||||
sexp_src = await render_calendar_oob(tctx)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
|
||||
@bp.put("/")
|
||||
@@ -201,7 +202,7 @@ def register():
|
||||
from sexp.sexp_components import _calendar_admin_main_panel_html
|
||||
ctx = await get_template_context()
|
||||
html = _calendar_admin_main_panel_html(ctx)
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(html)
|
||||
|
||||
|
||||
@bp.delete("/")
|
||||
@@ -238,7 +239,7 @@ def register():
|
||||
nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
|
||||
html = html + nav_oob
|
||||
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(html)
|
||||
|
||||
|
||||
return bp
|
||||
|
||||
@@ -16,6 +16,7 @@ from .services.entries import (
|
||||
)
|
||||
from shared.browser.app.authz import require_admin
|
||||
from shared.browser.app.redis_cacher import clear_cache
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
from bp.calendar_entry.routes import register as register_calendar_entry
|
||||
@@ -260,7 +261,7 @@ def register():
|
||||
from sexp.sexp_components import render_day_main_panel
|
||||
html = render_day_main_panel(ctx)
|
||||
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
|
||||
return await make_response(html + mini_html, 200)
|
||||
return sexp_response(html + (mini_html or ""))
|
||||
|
||||
@bp.get("/add/")
|
||||
async def add_form(day: int, month: int, year: int, **kwargs):
|
||||
|
||||
@@ -28,6 +28,7 @@ import math
|
||||
import logging
|
||||
|
||||
from shared.infrastructure.fragments import fetch_fragment
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
from ..ticket_types.routes import register as register_ticket_types
|
||||
|
||||
@@ -216,12 +217,12 @@ def register():
|
||||
)
|
||||
|
||||
# Fetch container nav from relations (exclude calendar — we're on a calendar page)
|
||||
container_nav_html = ""
|
||||
container_nav = ""
|
||||
post_data = getattr(g, "post_data", None)
|
||||
if post_data:
|
||||
post_id = post_data["post"]["id"]
|
||||
post_slug = post_data["post"]["slug"]
|
||||
container_nav_html = await fetch_fragment("relations", "container-nav", params={
|
||||
container_nav = await fetch_fragment("relations", "container-nav", params={
|
||||
"container_type": "page",
|
||||
"container_id": str(post_id),
|
||||
"post_slug": post_slug,
|
||||
@@ -235,7 +236,7 @@ def register():
|
||||
"ticket_sold_count": ticket_sold_count,
|
||||
"user_ticket_count": user_ticket_count,
|
||||
"user_ticket_counts_by_type": user_ticket_counts_by_type,
|
||||
"container_nav_html": container_nav_html,
|
||||
"container_nav": container_nav,
|
||||
}
|
||||
@bp.get("/")
|
||||
@require_admin
|
||||
@@ -247,10 +248,10 @@ def register():
|
||||
tctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_entry_page(tctx)
|
||||
return await make_response(html, 200)
|
||||
else:
|
||||
html = await render_entry_oob(tctx)
|
||||
|
||||
return await make_response(html, 200)
|
||||
sexp_src = await render_entry_oob(tctx)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.get("/edit/")
|
||||
@require_admin
|
||||
@@ -423,7 +424,7 @@ def register():
|
||||
|
||||
tctx = await get_template_context()
|
||||
html = await render_entry_page(tctx)
|
||||
return await make_response(html + nav_oob, 200)
|
||||
return sexp_response(html + nav_oob)
|
||||
|
||||
|
||||
@bp.post("/confirm/")
|
||||
@@ -449,7 +450,7 @@ def register():
|
||||
await g.s.refresh(g.entry)
|
||||
from sexp.sexp_components import render_entry_optioned
|
||||
html = render_entry_optioned(g.entry, g.calendar, day, month, year)
|
||||
return await make_response(html + day_nav_oob + post_nav_oob, 200)
|
||||
return sexp_response(html + day_nav_oob + post_nav_oob)
|
||||
|
||||
@bp.post("/decline/")
|
||||
@require_admin
|
||||
@@ -474,7 +475,7 @@ def register():
|
||||
await g.s.refresh(g.entry)
|
||||
from sexp.sexp_components import render_entry_optioned
|
||||
html = render_entry_optioned(g.entry, g.calendar, day, month, year)
|
||||
return await make_response(html + day_nav_oob + post_nav_oob, 200)
|
||||
return sexp_response(html + day_nav_oob + post_nav_oob)
|
||||
|
||||
@bp.post("/provisional/")
|
||||
@require_admin
|
||||
@@ -499,7 +500,7 @@ def register():
|
||||
await g.s.refresh(g.entry)
|
||||
from sexp.sexp_components import render_entry_optioned
|
||||
html = render_entry_optioned(g.entry, g.calendar, day, month, year)
|
||||
return await make_response(html + day_nav_oob + post_nav_oob, 200)
|
||||
return sexp_response(html + day_nav_oob + post_nav_oob)
|
||||
|
||||
@bp.post("/tickets/")
|
||||
@require_admin
|
||||
@@ -543,7 +544,7 @@ def register():
|
||||
await g.s.refresh(g.entry)
|
||||
from sexp.sexp_components import render_entry_tickets_config
|
||||
html = render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year"))
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(html)
|
||||
|
||||
@bp.get("/posts/search/")
|
||||
@require_admin
|
||||
@@ -596,7 +597,7 @@ def register():
|
||||
va = request.view_args or {}
|
||||
html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
|
||||
nav_oob = render_entry_posts_nav_oob(entry_posts)
|
||||
return await make_response(html + nav_oob, 200)
|
||||
return sexp_response(html + nav_oob)
|
||||
|
||||
@bp.delete("/posts/<int:post_id>/")
|
||||
@require_admin
|
||||
@@ -618,6 +619,6 @@ def register():
|
||||
va = request.view_args or {}
|
||||
html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
|
||||
nav_oob = render_entry_posts_nav_oob(entry_posts)
|
||||
return await make_response(html + nav_oob, 200)
|
||||
return sexp_response(html + nav_oob)
|
||||
|
||||
return bp
|
||||
|
||||
@@ -16,6 +16,7 @@ from shared.browser.app.redis_cacher import cache_page, clear_cache
|
||||
|
||||
from shared.browser.app.authz import require_admin
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -36,9 +37,10 @@ def register():
|
||||
ctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_calendars_page(ctx)
|
||||
return await make_response(html)
|
||||
else:
|
||||
html = await render_calendars_oob(ctx)
|
||||
return await make_response(html)
|
||||
sexp_src = await render_calendars_oob(ctx)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
|
||||
@bp.post("/new/")
|
||||
@@ -86,5 +88,5 @@ def register():
|
||||
nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
|
||||
html = html + nav_oob
|
||||
|
||||
return await make_response(html)
|
||||
return sexp_response(html)
|
||||
return bp
|
||||
|
||||
@@ -6,6 +6,7 @@ from quart import (
|
||||
|
||||
|
||||
from shared.browser.app.authz import require_admin
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -23,8 +24,8 @@ def register():
|
||||
tctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_day_admin_page(tctx)
|
||||
return await make_response(html)
|
||||
else:
|
||||
html = await render_day_admin_oob(tctx)
|
||||
|
||||
return await make_response(html)
|
||||
sexp_src = await render_day_admin_oob(tctx)
|
||||
return sexp_response(sexp_src)
|
||||
return bp
|
||||
|
||||
@@ -18,6 +18,7 @@ from models.calendars import CalendarSlot # add this import
|
||||
from sqlalchemy import select
|
||||
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -79,12 +80,12 @@ def register():
|
||||
day_slots = list(result.scalars())
|
||||
|
||||
# Fetch container nav from relations (exclude calendar — we're on a calendar page)
|
||||
container_nav_html = ""
|
||||
container_nav = ""
|
||||
post_data = getattr(g, "post_data", None)
|
||||
if post_data:
|
||||
post_id = post_data["post"]["id"]
|
||||
post_slug = post_data["post"]["slug"]
|
||||
container_nav_html = await fetch_fragment("relations", "container-nav", params={
|
||||
container_nav = await fetch_fragment("relations", "container-nav", params={
|
||||
"container_type": "page",
|
||||
"container_id": str(post_id),
|
||||
"post_slug": post_slug,
|
||||
@@ -101,7 +102,7 @@ def register():
|
||||
"user_entries": visible.user_entries,
|
||||
"confirmed_entries": visible.confirmed_entries,
|
||||
"day_slots": day_slots,
|
||||
"container_nav_html": container_nav_html,
|
||||
"container_nav": container_nav,
|
||||
}
|
||||
|
||||
|
||||
@@ -127,9 +128,10 @@ def register():
|
||||
tctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_day_page(tctx)
|
||||
return await make_response(html)
|
||||
else:
|
||||
html = await render_day_oob(tctx)
|
||||
return await make_response(html)
|
||||
sexp_src = await render_day_oob(tctx)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.get("/w/<widget_domain>/")
|
||||
async def widget_paginate(widget_domain: str, **kwargs):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Events app fragment endpoints.
|
||||
|
||||
Exposes HTML fragments at ``/internal/fragments/<type>`` for consumption
|
||||
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption
|
||||
by other coop apps via the fragment client.
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,9 @@ def register():
|
||||
|
||||
_handlers: dict[str, object] = {}
|
||||
|
||||
# Fragment types that still return HTML (Jinja templates)
|
||||
_html_types = {"container-cards", "account-page"}
|
||||
|
||||
@bp.before_request
|
||||
async def _require_fragment_header():
|
||||
if not request.headers.get(FRAGMENT_HEADER):
|
||||
@@ -28,16 +31,17 @@ def register():
|
||||
async def get_fragment(fragment_type: str):
|
||||
handler = _handlers.get(fragment_type)
|
||||
if handler is None:
|
||||
return Response("", status=200, content_type="text/html")
|
||||
html = await handler()
|
||||
return Response(html, status=200, content_type="text/html")
|
||||
return Response("", status=200, content_type="text/sexp")
|
||||
result = await handler()
|
||||
ct = "text/html" if fragment_type in _html_types else "text/sexp"
|
||||
return Response(result, status=200, content_type=ct)
|
||||
|
||||
# --- container-nav fragment: calendar entries + calendar links -----------
|
||||
|
||||
async def _container_nav_handler():
|
||||
from quart import current_app
|
||||
from shared.infrastructure.urls import events_url
|
||||
from shared.sexp.jinja_bridge import render as render_comp
|
||||
from shared.sexp.helpers import sexp_call
|
||||
|
||||
container_type = request.args.get("container_type", "page")
|
||||
container_id = int(request.args.get("container_id", 0))
|
||||
@@ -49,7 +53,7 @@ def register():
|
||||
|
||||
styles = current_app.jinja_env.globals.get("styles", {})
|
||||
nav_class = styles.get("nav_button_less_pad", "")
|
||||
html_parts = []
|
||||
parts = []
|
||||
|
||||
# Calendar entries nav
|
||||
if not any(e.startswith("calendar") for e in excludes):
|
||||
@@ -65,21 +69,16 @@ def register():
|
||||
date_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
|
||||
if entry.end_at:
|
||||
date_str += f" – {entry.end_at.strftime('%H:%M')}"
|
||||
html_parts.append(render_comp(
|
||||
"calendar-entry-nav",
|
||||
parts.append(sexp_call("calendar-entry-nav",
|
||||
href=events_url(entry_path), name=entry.name,
|
||||
date_str=date_str, nav_class=nav_class,
|
||||
))
|
||||
# Infinite scroll sentinel (kept as raw HTML — HTMX-specific)
|
||||
date_str=date_str, nav_class=nav_class))
|
||||
if has_more and paginate_url_base:
|
||||
html_parts.append(render_comp(
|
||||
"htmx-sentinel",
|
||||
parts.append(sexp_call("htmx-sentinel",
|
||||
id=f"entries-load-sentinel-{page}",
|
||||
hx_get=f"{paginate_url_base}?page={page + 1}",
|
||||
hx_trigger="intersect once",
|
||||
hx_swap="beforebegin",
|
||||
**{"class": "flex-shrink-0 w-1"},
|
||||
))
|
||||
**{"class": "flex-shrink-0 w-1"}))
|
||||
|
||||
# Calendar links nav
|
||||
if not any(e.startswith("calendar") for e in excludes):
|
||||
@@ -88,16 +87,16 @@ def register():
|
||||
)
|
||||
for cal in calendars:
|
||||
href = events_url(f"/{post_slug}/{cal.slug}/")
|
||||
html_parts.append(render_comp(
|
||||
"calendar-link-nav",
|
||||
href=href, name=cal.name, nav_class=nav_class,
|
||||
))
|
||||
parts.append(sexp_call("calendar-link-nav",
|
||||
href=href, name=cal.name, nav_class=nav_class))
|
||||
|
||||
return "\n".join(html_parts)
|
||||
if not parts:
|
||||
return ""
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
|
||||
_handlers["container-nav"] = _container_nav_handler
|
||||
|
||||
# --- container-cards fragment: entries for blog listing cards ------------
|
||||
# --- container-cards fragment: entries for blog listing cards (still Jinja) --
|
||||
|
||||
async def _container_cards_handler():
|
||||
post_ids_raw = request.args.get("post_ids", "")
|
||||
@@ -107,7 +106,6 @@ def register():
|
||||
if not post_ids:
|
||||
return ""
|
||||
|
||||
# Build post_id -> slug mapping
|
||||
slug_map = {}
|
||||
for i, pid in enumerate(post_ids):
|
||||
slug_map[pid] = post_slugs[i] if i < len(post_slugs) else ""
|
||||
@@ -120,12 +118,12 @@ def register():
|
||||
|
||||
_handlers["container-cards"] = _container_cards_handler
|
||||
|
||||
# --- account-nav-item fragment: tickets + bookings links for account nav -
|
||||
# --- account-nav-item fragment: tickets + bookings links -----------------
|
||||
|
||||
async def _account_nav_item_handler():
|
||||
from quart import current_app
|
||||
from shared.infrastructure.urls import account_url
|
||||
from shared.sexp.jinja_bridge import render as render_comp
|
||||
from shared.sexp.helpers import sexp_call
|
||||
|
||||
styles = current_app.jinja_env.globals.get("styles", {})
|
||||
nav_class = styles.get("nav_button", "")
|
||||
@@ -135,19 +133,15 @@ def register():
|
||||
)
|
||||
tickets_url = account_url("/tickets/")
|
||||
bookings_url = account_url("/bookings/")
|
||||
# These two links use HTMX navigation — kept as raw HTML for the
|
||||
# hx-* attributes that don't map neatly to a reusable component.
|
||||
parts = []
|
||||
for href, label in [(tickets_url, "tickets"), (bookings_url, "bookings")]:
|
||||
parts.append(render_comp(
|
||||
"nav-group-link",
|
||||
href=href, hx_select=hx_select, nav_class=nav_class, label=label,
|
||||
))
|
||||
return "\n".join(parts)
|
||||
parts.append(sexp_call("nav-group-link",
|
||||
href=href, hx_select=hx_select, nav_class=nav_class, label=label))
|
||||
return "(<> " + " ".join(parts) + ")"
|
||||
|
||||
_handlers["account-nav-item"] = _account_nav_item_handler
|
||||
|
||||
# --- account-page fragment: tickets or bookings panel --------------------
|
||||
# --- account-page fragment: tickets or bookings panel (still Jinja) ------
|
||||
|
||||
async def _account_page_handler():
|
||||
slug = request.args.get("slug", "")
|
||||
@@ -171,15 +165,21 @@ def register():
|
||||
|
||||
_handlers["account-page"] = _account_page_handler
|
||||
|
||||
# --- link-card fragment: event page preview card ----------------------------
|
||||
# --- link-card fragment: event page preview card -------------------------
|
||||
|
||||
async def _link_card_handler():
|
||||
from shared.infrastructure.urls import events_url
|
||||
from shared.sexp.jinja_bridge import render as render_comp
|
||||
from shared.sexp.helpers import sexp_call
|
||||
|
||||
slug = request.args.get("slug", "")
|
||||
keys_raw = request.args.get("keys", "")
|
||||
|
||||
def _event_link_card_sexp(post, cal_names: str) -> str:
|
||||
return sexp_call("link-card",
|
||||
title=post.title, image=post.feature_image,
|
||||
subtitle=cal_names,
|
||||
link=events_url(f"/{post.slug}"))
|
||||
|
||||
# Batch mode
|
||||
if keys_raw:
|
||||
slugs = [k.strip() for k in keys_raw.split(",") if k.strip()]
|
||||
@@ -193,11 +193,7 @@ def register():
|
||||
g.s, "page", post.id,
|
||||
)
|
||||
cal_names = ", ".join(c.name for c in calendars) if calendars else ""
|
||||
parts.append(render_comp(
|
||||
"link-card",
|
||||
title=post.title, image=post.feature_image,
|
||||
subtitle=cal_names, link=events_url(f"/{post.slug}"),
|
||||
))
|
||||
parts.append(_event_link_card_sexp(post, cal_names))
|
||||
return "\n".join(parts)
|
||||
|
||||
# Single mode
|
||||
@@ -211,11 +207,7 @@ def register():
|
||||
g.s, "page", post.id,
|
||||
)
|
||||
cal_names = ", ".join(c.name for c in calendars) if calendars else ""
|
||||
return render_comp(
|
||||
"link-card",
|
||||
title=post.title, image=post.feature_image,
|
||||
subtitle=cal_names, link=events_url(f"/{post.slug}"),
|
||||
)
|
||||
return _event_link_card_sexp(post, cal_names)
|
||||
|
||||
_handlers["link-card"] = _link_card_handler
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from .services.markets import (
|
||||
from shared.browser.app.redis_cacher import cache_page, clear_cache
|
||||
from shared.browser.app.authz import require_admin
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -29,9 +30,10 @@ def register():
|
||||
ctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_markets_page(ctx)
|
||||
return await make_response(html)
|
||||
else:
|
||||
html = await render_markets_oob(ctx)
|
||||
return await make_response(html)
|
||||
sexp_src = await render_markets_oob(ctx)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.post("/new/")
|
||||
@require_admin
|
||||
@@ -56,8 +58,7 @@ def register():
|
||||
from shared.sexp.page import get_template_context
|
||||
from sexp.sexp_components import render_markets_list_panel
|
||||
ctx = await get_template_context()
|
||||
html = render_markets_list_panel(ctx)
|
||||
return await make_response(html)
|
||||
return sexp_response(render_markets_list_panel(ctx))
|
||||
|
||||
@bp.delete("/<market_slug>/")
|
||||
@require_admin
|
||||
@@ -70,7 +71,6 @@ def register():
|
||||
from shared.sexp.page import get_template_context
|
||||
from sexp.sexp_components import render_markets_list_panel
|
||||
ctx = await get_template_context()
|
||||
html = render_markets_list_panel(ctx)
|
||||
return await make_response(html)
|
||||
return sexp_response(render_markets_list_panel(ctx))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
from quart import Blueprint, g, request, render_template, make_response
|
||||
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.services.registry import services
|
||||
|
||||
@@ -50,11 +51,11 @@ def register() -> Blueprint:
|
||||
|
||||
ctx = await get_template_context()
|
||||
if is_htmx_request():
|
||||
html = await render_page_summary_oob(ctx, entries, has_more, pending_tickets, {}, page, view)
|
||||
sexp_src = await render_page_summary_oob(ctx, entries, has_more, pending_tickets, {}, page, view)
|
||||
return sexp_response(sexp_src)
|
||||
else:
|
||||
html = await render_page_summary_page(ctx, entries, has_more, pending_tickets, {}, page, view)
|
||||
|
||||
return await make_response(html, 200)
|
||||
return await make_response(html, 200)
|
||||
|
||||
@bp.get("/entries")
|
||||
async def entries_fragment():
|
||||
@@ -65,8 +66,8 @@ def register() -> Blueprint:
|
||||
entries, has_more, pending_tickets = await _load_entries(post["id"], page)
|
||||
|
||||
from sexp.sexp_components import render_page_summary_cards
|
||||
html = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post)
|
||||
return await make_response(html, 200)
|
||||
sexp_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.post("/tickets/adjust")
|
||||
async def adjust_ticket():
|
||||
@@ -108,6 +109,6 @@ def register() -> Blueprint:
|
||||
from sexp.sexp_components import render_ticket_widget
|
||||
widget_html = render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust")
|
||||
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
|
||||
return await make_response(widget_html + mini_html, 200)
|
||||
return sexp_response(widget_html + (mini_html or ""))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -24,6 +24,7 @@ from shared.browser.app.utils import (
|
||||
parse_cost
|
||||
)
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -75,8 +76,7 @@ def register():
|
||||
if not slot:
|
||||
return await make_response("Not found", 404)
|
||||
from sexp.sexp_components import render_slot_main_panel
|
||||
html = render_slot_main_panel(slot, g.calendar)
|
||||
return await make_response(html)
|
||||
return sexp_response(render_slot_main_panel(slot, g.calendar))
|
||||
|
||||
@bp.delete("/")
|
||||
@require_admin
|
||||
@@ -85,8 +85,7 @@ def register():
|
||||
await svc_delete_slot(g.s, slot_id)
|
||||
slots = await svc_list_slots(g.s, g.calendar.id)
|
||||
from sexp.sexp_components import render_slots_table
|
||||
html = render_slots_table(slots, g.calendar)
|
||||
return await make_response(html)
|
||||
return sexp_response(render_slots_table(slots, g.calendar))
|
||||
|
||||
@bp.put("/")
|
||||
@require_admin
|
||||
@@ -168,8 +167,7 @@ def register():
|
||||
), 422
|
||||
|
||||
from sexp.sexp_components import render_slot_main_panel
|
||||
html = render_slot_main_panel(slot, g.calendar, oob=True)
|
||||
return await make_response(html)
|
||||
return sexp_response(render_slot_main_panel(slot, g.calendar, oob=True))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from shared.browser.app.utils import (
|
||||
parse_cost
|
||||
)
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -50,9 +51,10 @@ def register():
|
||||
tctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_slots_page(tctx)
|
||||
return await make_response(html)
|
||||
else:
|
||||
html = await render_slots_oob(tctx)
|
||||
return await make_response(html)
|
||||
sexp_src = await render_slots_oob(tctx)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
|
||||
@bp.post("/")
|
||||
@@ -128,8 +130,7 @@ def register():
|
||||
# Success → re-render the slots table
|
||||
slots = await svc_list_slots(g.s, g.calendar.id)
|
||||
from sexp.sexp_components import render_slots_table
|
||||
html = render_slots_table(slots, g.calendar)
|
||||
return await make_response(html)
|
||||
return sexp_response(render_slots_table(slots, g.calendar))
|
||||
|
||||
|
||||
@bp.get("/add")
|
||||
|
||||
@@ -20,6 +20,7 @@ from sqlalchemy.orm import selectinload
|
||||
from models.calendars import CalendarEntry, Ticket, TicketType
|
||||
from shared.browser.app.authz import require_admin
|
||||
from shared.browser.app.redis_cacher import clear_cache
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
from ..tickets.services.tickets import (
|
||||
get_ticket_by_code,
|
||||
@@ -76,10 +77,10 @@ def register() -> Blueprint:
|
||||
ctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_ticket_admin_page(ctx, tickets, stats)
|
||||
return await make_response(html, 200)
|
||||
else:
|
||||
html = await render_ticket_admin_oob(ctx, tickets, stats)
|
||||
|
||||
return await make_response(html, 200)
|
||||
sexp_src = await render_ticket_admin_oob(ctx, tickets, stats)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.get("/entry/<int:entry_id>/")
|
||||
@require_admin
|
||||
@@ -102,7 +103,7 @@ def register() -> Blueprint:
|
||||
|
||||
from sexp.sexp_components import render_entry_tickets_admin
|
||||
html = render_entry_tickets_admin(entry, tickets)
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(html)
|
||||
|
||||
@bp.get("/lookup/")
|
||||
@require_admin
|
||||
@@ -118,11 +119,9 @@ def register() -> Blueprint:
|
||||
ticket = await get_ticket_by_code(g.s, code)
|
||||
from sexp.sexp_components import render_lookup_result
|
||||
if not ticket:
|
||||
html = render_lookup_result(None, "Ticket not found")
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(render_lookup_result(None, "Ticket not found"))
|
||||
|
||||
html = render_lookup_result(ticket, None)
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(render_lookup_result(ticket, None))
|
||||
|
||||
@bp.post("/<code>/checkin/")
|
||||
@require_admin
|
||||
@@ -133,11 +132,9 @@ def register() -> Blueprint:
|
||||
|
||||
from sexp.sexp_components import render_checkin_result
|
||||
if not success:
|
||||
html = render_checkin_result(False, error, None)
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(render_checkin_result(False, error, None))
|
||||
|
||||
ticket = await get_ticket_by_code(g.s, code)
|
||||
html = render_checkin_result(True, None, ticket)
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(render_checkin_result(True, None, ticket))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -17,6 +17,7 @@ from ..ticket_types.services.tickets import (
|
||||
list_ticket_types as svc_list_ticket_types,
|
||||
)
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -68,11 +69,10 @@ def register():
|
||||
|
||||
from sexp.sexp_components import render_ticket_type_main_panel
|
||||
va = request.view_args or {}
|
||||
html = render_ticket_type_main_panel(
|
||||
return sexp_response(render_ticket_type_main_panel(
|
||||
ticket_type, g.entry, g.calendar,
|
||||
va.get("day"), va.get("month"), va.get("year"),
|
||||
)
|
||||
return await make_response(html)
|
||||
))
|
||||
|
||||
@bp.put("/")
|
||||
@require_admin
|
||||
@@ -136,12 +136,11 @@ def register():
|
||||
# Return updated view with OOB flag
|
||||
from sexp.sexp_components import render_ticket_type_main_panel
|
||||
va = request.view_args or {}
|
||||
html = render_ticket_type_main_panel(
|
||||
return sexp_response(render_ticket_type_main_panel(
|
||||
ticket_type, g.entry, g.calendar,
|
||||
va.get("day"), va.get("month"), va.get("year"),
|
||||
oob=True,
|
||||
)
|
||||
return await make_response(html)
|
||||
))
|
||||
|
||||
@bp.delete("/")
|
||||
@require_admin
|
||||
@@ -156,10 +155,9 @@ def register():
|
||||
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
|
||||
from sexp.sexp_components import render_ticket_types_table
|
||||
va = request.view_args or {}
|
||||
html = render_ticket_types_table(
|
||||
return sexp_response(render_ticket_types_table(
|
||||
ticket_types, g.entry, g.calendar,
|
||||
va.get("day"), va.get("month"), va.get("year"),
|
||||
)
|
||||
return await make_response(html)
|
||||
))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -15,6 +15,7 @@ from .services.tickets import (
|
||||
from ..ticket_type.routes import register as register_ticket_type
|
||||
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
|
||||
def register():
|
||||
@@ -111,11 +112,10 @@ def register():
|
||||
ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
|
||||
from sexp.sexp_components import render_ticket_types_table
|
||||
va = request.view_args or {}
|
||||
html = render_ticket_types_table(
|
||||
return sexp_response(render_ticket_types_table(
|
||||
ticket_types, g.entry, g.calendar,
|
||||
va.get("day"), va.get("month"), va.get("year"),
|
||||
)
|
||||
return await make_response(html)
|
||||
))
|
||||
|
||||
@bp.get("/add")
|
||||
@require_admin
|
||||
|
||||
@@ -20,6 +20,7 @@ from sqlalchemy.orm import selectinload
|
||||
from models.calendars import CalendarEntry
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.browser.app.redis_cacher import clear_cache
|
||||
from shared.sexp.helpers import sexp_response
|
||||
|
||||
from .services.tickets import (
|
||||
create_ticket,
|
||||
@@ -56,10 +57,10 @@ def register() -> Blueprint:
|
||||
ctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_tickets_page(ctx, tickets)
|
||||
return await make_response(html, 200)
|
||||
else:
|
||||
html = await render_tickets_oob(ctx, tickets)
|
||||
|
||||
return await make_response(html, 200)
|
||||
sexp_src = await render_tickets_oob(ctx, tickets)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.get("/<code>/")
|
||||
async def ticket_detail(code: str):
|
||||
@@ -87,10 +88,10 @@ def register() -> Blueprint:
|
||||
ctx = await get_template_context()
|
||||
if not is_htmx_request():
|
||||
html = await render_ticket_detail_page(ctx, ticket)
|
||||
return await make_response(html, 200)
|
||||
else:
|
||||
html = await render_ticket_detail_oob(ctx, ticket)
|
||||
|
||||
return await make_response(html, 200)
|
||||
sexp_src = await render_ticket_detail_oob(ctx, ticket)
|
||||
return sexp_response(sexp_src)
|
||||
|
||||
@bp.post("/buy/")
|
||||
@clear_cache(tag="calendars", tag_scope="all")
|
||||
@@ -182,8 +183,7 @@ def register() -> Blueprint:
|
||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||
|
||||
from sexp.sexp_components import render_buy_result
|
||||
html = render_buy_result(entry, created, remaining, cart_count)
|
||||
return await make_response(html, 200)
|
||||
return sexp_response(render_buy_result(entry, created, remaining, cart_count))
|
||||
|
||||
@bp.post("/adjust/")
|
||||
@clear_cache(tag="calendars", tag_scope="all")
|
||||
@@ -306,11 +306,9 @@ def register() -> Blueprint:
|
||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||
|
||||
from sexp.sexp_components import render_adjust_response
|
||||
html = render_adjust_response(
|
||||
return sexp_response(render_adjust_response(
|
||||
entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type, cart_count,
|
||||
)
|
||||
|
||||
return await make_response(html, 200)
|
||||
))
|
||||
|
||||
return bp
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
;; Events admin components
|
||||
|
||||
(defcomp ~events-calendar-admin-panel (&key description-html csrf description)
|
||||
(defcomp ~events-calendar-admin-panel (&key description-content csrf description)
|
||||
(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! description-html))
|
||||
(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()"
|
||||
(when description-content description-content))
|
||||
(form :id "calendar-form" :method "post" :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-on:beforeRequest "document.querySelector('#cal-put-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#cal-put-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
:sx-on:afterRequest "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")
|
||||
@@ -23,10 +23,10 @@
|
||||
(a :href href :class "inline-flex items-center gap-1 px-2 py-1 text-xs text-stone-500 hover:text-stone-700 hover:bg-stone-100 rounded"
|
||||
(i :class "fa fa-cog" :aria-hidden "true") " Admin"))
|
||||
|
||||
(defcomp ~events-entry-field (&key label content-html)
|
||||
(defcomp ~events-entry-field (&key label content)
|
||||
(div :class "flex flex-col mb-4"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" label)
|
||||
(raw! content-html)))
|
||||
content))
|
||||
|
||||
(defcomp ~events-entry-name-field (&key name)
|
||||
(div :class "mt-1 text-lg font-medium" name))
|
||||
@@ -42,50 +42,50 @@
|
||||
(defcomp ~events-entry-time-field (&key time-str)
|
||||
(div :class "mt-1" time-str))
|
||||
|
||||
(defcomp ~events-entry-state-field (&key entry-id badge-html)
|
||||
(div :class "mt-1" (div :id (str "entry-state-" entry-id) (raw! badge-html))))
|
||||
(defcomp ~events-entry-state-field (&key entry-id badge)
|
||||
(div :class "mt-1" (div :id (str "entry-state-" entry-id) badge)))
|
||||
|
||||
(defcomp ~events-entry-cost-field (&key cost-html)
|
||||
(div :class "mt-1" (span :class "font-medium text-green-600" (raw! cost-html))))
|
||||
(defcomp ~events-entry-cost-field (&key cost)
|
||||
(div :class "mt-1" (span :class "font-medium text-green-600" cost)))
|
||||
|
||||
(defcomp ~events-entry-tickets-field (&key entry-id tickets-config-html)
|
||||
(div :class "mt-1" :id (str "entry-tickets-" entry-id) (raw! tickets-config-html)))
|
||||
(defcomp ~events-entry-tickets-field (&key entry-id tickets-config)
|
||||
(div :class "mt-1" :id (str "entry-tickets-" entry-id) tickets-config))
|
||||
|
||||
(defcomp ~events-entry-date-field (&key date-str)
|
||||
(div :class "mt-1" date-str))
|
||||
|
||||
(defcomp ~events-entry-posts-field (&key entry-id posts-panel-html)
|
||||
(div :class "mt-1" :id (str "entry-posts-" entry-id) (raw! posts-panel-html)))
|
||||
(defcomp ~events-entry-posts-field (&key entry-id posts-panel)
|
||||
(div :class "mt-1" :id (str "entry-posts-" entry-id) posts-panel))
|
||||
|
||||
(defcomp ~events-entry-panel (&key entry-id list-container name-html slot-html time-html state-html cost-html
|
||||
tickets-html buy-html date-html posts-html options-html pre-action edit-url)
|
||||
(defcomp ~events-entry-panel (&key entry-id list-container name slot time state cost
|
||||
tickets buy date posts options pre-action edit-url)
|
||||
(section :id (str "entry-" entry-id) :class list-container
|
||||
(raw! name-html) (raw! slot-html) (raw! time-html) (raw! state-html) (raw! cost-html)
|
||||
(raw! tickets-html) (raw! buy-html) (raw! date-html) (raw! posts-html)
|
||||
name slot time state cost
|
||||
tickets buy date posts
|
||||
(div :class "flex gap-2 mt-6"
|
||||
(raw! options-html)
|
||||
options
|
||||
(button :type "button" :class pre-action
|
||||
:hx-get edit-url :hx-target (str "#entry-" entry-id) :hx-swap "outerHTML"
|
||||
:sx-get edit-url :sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
|
||||
"Edit"))))
|
||||
|
||||
(defcomp ~events-entry-title (&key name badge-html)
|
||||
(<> (i :class "fa fa-clock") " " name " " (raw! badge-html)))
|
||||
(defcomp ~events-entry-title (&key name badge)
|
||||
(<> (i :class "fa fa-clock") " " name " " badge))
|
||||
|
||||
(defcomp ~events-entry-times (&key time-str)
|
||||
(div :class "text-sm text-gray-600" time-str))
|
||||
|
||||
(defcomp ~events-entry-optioned-oob (&key entry-id title-html state-html)
|
||||
(<> (div :id (str "entry-title-" entry-id) :hx-swap-oob "innerHTML" (raw! title-html))
|
||||
(div :id (str "entry-state-" entry-id) :hx-swap-oob "innerHTML" (raw! state-html))))
|
||||
(defcomp ~events-entry-optioned-oob (&key entry-id title state)
|
||||
(<> (div :id (str "entry-title-" entry-id) :sx-swap-oob "innerHTML" title)
|
||||
(div :id (str "entry-state-" entry-id) :sx-swap-oob "innerHTML" state)))
|
||||
|
||||
(defcomp ~events-entry-options (&key entry-id buttons-html)
|
||||
(defcomp ~events-entry-options (&key entry-id buttons)
|
||||
(div :id (str "calendar_entry_options_" entry-id) :class "flex flex-col md:flex-row gap-1"
|
||||
(raw! buttons-html)))
|
||||
buttons))
|
||||
|
||||
(defcomp ~events-entry-option-button (&key url target csrf btn-type action-btn confirm-title confirm-text
|
||||
label is-btn)
|
||||
(form :hx-post url :hx-select target :hx-target target :hx-swap "outerHTML"
|
||||
:hx-trigger (if is-btn "confirmed" nil)
|
||||
(form :sx-post url :sx-select target :sx-target target :sx-swap "outerHTML"
|
||||
:sx-trigger (if is-btn "confirmed" nil)
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type btn-type :class action-btn
|
||||
:data-confirm "true" :data-confirm-title confirm-title
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
(defcomp ~events-calendar-nav-arrow (&key pill-cls href label)
|
||||
(a :class (str pill-cls " text-xl") :href href
|
||||
:hx-get href :hx-target "#main-panel" :hx-select "#main-panel" :hx-swap "outerHTML" :hx-push-url "true" label))
|
||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true" label))
|
||||
|
||||
(defcomp ~events-calendar-month-label (&key month-name year)
|
||||
(div :class "px-3 font-medium" (str month-name " " year)))
|
||||
@@ -14,35 +14,35 @@
|
||||
(span :class "sm:hidden text-[16px] text-stone-500" day-str))
|
||||
|
||||
(defcomp ~events-calendar-day-num (&key pill-cls href num)
|
||||
(a :class pill-cls :href href :hx-get href :hx-target "#main-panel" :hx-select "#main-panel"
|
||||
:hx-swap "outerHTML" :hx-push-url "true" num))
|
||||
(a :class pill-cls :href href :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true" num))
|
||||
|
||||
(defcomp ~events-calendar-entry-badge (&key bg-cls name state-label)
|
||||
(div :class (str "flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5 " bg-cls)
|
||||
(span :class "truncate" name)
|
||||
(span :class "shrink-0 text-[10px] font-semibold uppercase tracking-tight" state-label)))
|
||||
|
||||
(defcomp ~events-calendar-cell (&key cell-cls day-short-html day-num-html badges-html)
|
||||
(defcomp ~events-calendar-cell (&key cell-cls day-short day-num badges)
|
||||
(div :class cell-cls
|
||||
(div :class "flex justify-between items-center"
|
||||
(div :class "flex flex-col" (raw! day-short-html) (raw! day-num-html)))
|
||||
(div :class "mt-1 space-y-0.5" (raw! badges-html))))
|
||||
(div :class "flex flex-col" day-short day-num))
|
||||
(div :class "mt-1 space-y-0.5" badges)))
|
||||
|
||||
(defcomp ~events-calendar-grid (&key arrows-html weekdays-html cells-html)
|
||||
(defcomp ~events-calendar-grid (&key arrows weekdays cells)
|
||||
(section :class "bg-orange-100"
|
||||
(header :class "flex items-center justify-center mt-2"
|
||||
(nav :class "flex items-center gap-2 text-2xl" (raw! arrows-html)))
|
||||
(nav :class "flex items-center gap-2 text-2xl" arrows))
|
||||
(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! weekdays-html))
|
||||
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" (raw! cells-html)))))
|
||||
(div :class "hidden sm:grid grid-cols-7 text-center text-md font-semibold text-stone-700 mb-2" weekdays)
|
||||
(div :class "grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden" cells))))
|
||||
|
||||
(defcomp ~events-calendars-create-form (&key create-url csrf)
|
||||
(<>
|
||||
(div :id "cal-create-errors" :class "mt-2 text-sm text-red-600")
|
||||
(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;"
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
:sx-target "#calendars-list" :sx-select "#calendars-list" :sx-swap "outerHTML"
|
||||
:sx-on:beforeRequest "document.querySelector('#cal-create-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#cal-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "flex-1"
|
||||
(label :class "block text-sm text-gray-600" "Name")
|
||||
@@ -50,10 +50,10 @@
|
||||
:placeholder "e.g. Events, Gigs, Meetings"))
|
||||
(button :type "submit" :class "border rounded px-3 py-2" "Add calendar"))))
|
||||
|
||||
(defcomp ~events-calendars-panel (&key form-html list-html)
|
||||
(defcomp ~events-calendars-panel (&key form list)
|
||||
(section :class "p-4"
|
||||
(raw! form-html)
|
||||
(div :id "calendars-list" :class "mt-6" (raw! list-html))))
|
||||
form
|
||||
(div :id "calendars-list" :class "mt-6" list)))
|
||||
|
||||
(defcomp ~events-calendars-empty ()
|
||||
(p :class "text-gray-500 mt-4" "No calendars yet. Create one above."))
|
||||
@@ -62,7 +62,7 @@
|
||||
(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 href
|
||||
:hx-get href :hx-target "#main-panel" :hx-select "#main-panel" :hx-swap "outerHTML" :hx-push-url "true"
|
||||
:sx-get href :sx-target "#main-panel" :sx-select "#main-panel" :sx-swap "outerHTML" :sx-push-url "true"
|
||||
(h3 :class "font-semibold" cal-name)
|
||||
(h4 :class "text-gray-500" (str "/" cal-slug "/")))
|
||||
(button :class "text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
|
||||
@@ -70,9 +70,9 @@
|
||||
: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 del-url :hx-trigger "confirmed"
|
||||
:hx-target "#calendars-list" :hx-select "#calendars-list" :hx-swap "outerHTML"
|
||||
:hx-headers csrf-hdr
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target "#calendars-list" :sx-select "#calendars-list" :sx-swap "outerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
(defcomp ~events-calendar-description-display (&key description edit-url)
|
||||
@@ -81,22 +81,22 @@
|
||||
(p :class "text-stone-700 whitespace-pre-line break-all" description)
|
||||
(p :class "text-stone-400 italic" "No description yet."))
|
||||
(button :type "button" :class "mt-2 text-xs underline"
|
||||
:hx-get edit-url :hx-target "#calendar-description" :hx-swap "outerHTML"
|
||||
:sx-get edit-url :sx-target "#calendar-description" :sx-swap "outerHTML"
|
||||
(i :class "fas fa-edit"))))
|
||||
|
||||
(defcomp ~events-calendar-description-title-oob (&key description)
|
||||
(div :id "calendar-description-title" :hx-swap-oob "outerHTML"
|
||||
(div :id "calendar-description-title" :sx-swap-oob "outerHTML"
|
||||
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
description))
|
||||
|
||||
(defcomp ~events-calendar-description-edit-form (&key save-url cancel-url csrf description)
|
||||
(div :id "calendar-description"
|
||||
(form :hx-post save-url :hx-target "#calendar-description" :hx-swap "outerHTML"
|
||||
(form :sx-post save-url :sx-target "#calendar-description" :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(textarea :name "description" :autocomplete "off" :rows "4"
|
||||
:class "w-full p-2 border rounded" description)
|
||||
(div :class "mt-2 flex gap-2 text-xs"
|
||||
(button :type "submit" :class "px-3 py-1 rounded bg-stone-800 text-white" "Save")
|
||||
(button :type "button" :class "px-3 py-1 rounded border"
|
||||
:hx-get cancel-url :hx-target "#calendar-description" :hx-swap "outerHTML"
|
||||
:sx-get cancel-url :sx-target "#calendar-description" :sx-swap "outerHTML"
|
||||
"Cancel")))))
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
(div :class "font-medium truncate" name)
|
||||
(div :class "text-xs text-stone-600 truncate" time-str))))
|
||||
|
||||
(defcomp ~events-day-entries-nav (&key inner-html)
|
||||
(defcomp ~events-day-entries-nav (&key inner)
|
||||
(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-html))))
|
||||
inner)))
|
||||
|
||||
(defcomp ~events-day-table (&key list-container rows-html pre-action add-url)
|
||||
(defcomp ~events-day-table (&key list-container rows pre-action add-url)
|
||||
(section :id "day-entries" :class list-container
|
||||
(table :class "w-full text-sm border table-fixed"
|
||||
(thead :class "bg-stone-100"
|
||||
@@ -23,10 +23,10 @@
|
||||
(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! rows-html)))
|
||||
(tbody rows))
|
||||
(div :id "entry-add-container" :class "mt-4"
|
||||
(button :type "button" :class pre-action
|
||||
:hx-get add-url :hx-target "#entry-add-container" :hx-swap "innerHTML"
|
||||
:sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
|
||||
"+ Add entry"))))
|
||||
|
||||
(defcomp ~events-day-empty-row ()
|
||||
@@ -34,20 +34,20 @@
|
||||
|
||||
(defcomp ~events-day-row-name (&key href pill-cls name)
|
||||
(td :class "p-2 align-top w-2/6" (div :class "font-medium"
|
||||
(a :href href :class pill-cls :hx-get href :hx-target "#main-panel" :hx-select "#main-panel"
|
||||
:hx-swap "outerHTML" :hx-push-url "true" name))))
|
||||
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true" name))))
|
||||
|
||||
(defcomp ~events-day-row-slot (&key href pill-cls slot-name time-str)
|
||||
(td :class "p-2 align-top w-1/6" (div :class "text-xs font-medium"
|
||||
(a :href href :class pill-cls :hx-get href :hx-target "#main-panel" :hx-select "#main-panel"
|
||||
:hx-swap "outerHTML" :hx-push-url "true" slot-name)
|
||||
(span :class "text-stone-600 font-normal" (raw! time-str)))))
|
||||
(a :href href :class pill-cls :sx-get href :sx-target "#main-panel" :sx-select "#main-panel"
|
||||
:sx-swap "outerHTML" :sx-push-url "true" slot-name)
|
||||
(span :class "text-stone-600 font-normal" time-str))))
|
||||
|
||||
(defcomp ~events-day-row-time (&key start end)
|
||||
(td :class "p-2 align-top w-1/6" (div :class "text-xs text-stone-600" (str start end))))
|
||||
|
||||
(defcomp ~events-day-row-state (&key state-id badge-html)
|
||||
(td :class "p-2 align-top w-1/6" (div :id state-id (raw! badge-html))))
|
||||
(defcomp ~events-day-row-state (&key state-id badge)
|
||||
(td :class "p-2 align-top w-1/6" (div :id state-id badge)))
|
||||
|
||||
(defcomp ~events-day-row-cost (&key cost-str)
|
||||
(td :class "p-2 align-top w-1/6" (span :class "font-medium text-green-600" cost-str)))
|
||||
@@ -63,19 +63,19 @@
|
||||
(defcomp ~events-day-row-actions ()
|
||||
(td :class "p-2 align-top w-1/6"))
|
||||
|
||||
(defcomp ~events-day-row (&key tr-cls name-html slot-html state-html cost-html tickets-html actions-html)
|
||||
(tr :class tr-cls (raw! name-html) (raw! slot-html) (raw! state-html) (raw! cost-html) (raw! tickets-html) (raw! actions-html)))
|
||||
(defcomp ~events-day-row (&key tr-cls name slot state cost tickets actions)
|
||||
(tr :class tr-cls name slot state cost tickets actions))
|
||||
|
||||
(defcomp ~events-day-admin-panel ()
|
||||
(div :class "p-4 text-sm text-stone-500" "Admin options"))
|
||||
|
||||
(defcomp ~events-day-entries-nav-oob-empty ()
|
||||
(div :id "day-entries-nav-wrapper" :hx-swap-oob "true"))
|
||||
(div :id "day-entries-nav-wrapper" :sx-swap-oob "true"))
|
||||
|
||||
(defcomp ~events-day-entries-nav-oob (&key items-html)
|
||||
(defcomp ~events-day-entries-nav-oob (&key items)
|
||||
(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" :hx-swap-oob "true"
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" (raw! items-html))))
|
||||
:id "day-entries-nav-wrapper" :sx-swap-oob "true"
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
|
||||
|
||||
(defcomp ~events-day-nav-entry (&key href nav-btn name time-str)
|
||||
(a :href href :class nav-btn
|
||||
|
||||
@@ -24,38 +24,38 @@
|
||||
(span :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700" name))
|
||||
|
||||
(defcomp ~events-entry-time-linked (&key href date-str)
|
||||
(<> (a :href href :class "hover:text-stone-700" date-str) (raw! " · ")))
|
||||
(<> (a :href href :class "hover:text-stone-700" date-str) " · "))
|
||||
|
||||
(defcomp ~events-entry-time-plain (&key date-str)
|
||||
(<> (span date-str) (raw! " · ")))
|
||||
(<> (span date-str) " · "))
|
||||
|
||||
(defcomp ~events-entry-cost (&key cost-html)
|
||||
(div :class "mt-1 text-sm font-medium text-green-600" (raw! cost-html)))
|
||||
(defcomp ~events-entry-cost (&key cost)
|
||||
(div :class "mt-1 text-sm font-medium text-green-600" cost))
|
||||
|
||||
(defcomp ~events-entry-card (&key title-html badges-html time-parts cost-html widget-html)
|
||||
(defcomp ~events-entry-card (&key title badges time-parts cost widget)
|
||||
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-4"
|
||||
(div :class "flex flex-col sm:flex-row sm:items-start justify-between gap-3"
|
||||
(div :class "flex-1 min-w-0"
|
||||
(raw! title-html)
|
||||
(div :class "flex flex-wrap items-center gap-1.5 mt-1" (raw! badges-html))
|
||||
(div :class "mt-1 text-sm text-stone-500" (raw! time-parts))
|
||||
(raw! cost-html))
|
||||
(raw! widget-html))))
|
||||
title
|
||||
(div :class "flex flex-wrap items-center gap-1.5 mt-1" badges)
|
||||
(div :class "mt-1 text-sm text-stone-500" time-parts)
|
||||
cost)
|
||||
widget)))
|
||||
|
||||
(defcomp ~events-entry-card-tile (&key title-html badges-html time-html cost-html widget-html)
|
||||
(defcomp ~events-entry-card-tile (&key title badges time cost widget)
|
||||
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 overflow-hidden"
|
||||
(div :class "p-3"
|
||||
(raw! title-html)
|
||||
(div :class "flex flex-wrap items-center gap-1 mt-1" (raw! badges-html))
|
||||
(div :class "mt-1 text-xs text-stone-500" (raw! time-html))
|
||||
(raw! cost-html))
|
||||
(raw! widget-html)))
|
||||
title
|
||||
(div :class "flex flex-wrap items-center gap-1 mt-1" badges)
|
||||
(div :class "mt-1 text-xs text-stone-500" time)
|
||||
cost)
|
||||
widget))
|
||||
|
||||
(defcomp ~events-entry-tile-widget-wrapper (&key widget-html)
|
||||
(div :class "border-t border-stone-100 px-3 py-2" (raw! widget-html)))
|
||||
(defcomp ~events-entry-tile-widget-wrapper (&key widget)
|
||||
(div :class "border-t border-stone-100 px-3 py-2" widget))
|
||||
|
||||
(defcomp ~events-entry-widget-wrapper (&key widget-html)
|
||||
(div :class "shrink-0" (raw! widget-html)))
|
||||
(defcomp ~events-entry-widget-wrapper (&key widget)
|
||||
(div :class "shrink-0" widget))
|
||||
|
||||
(defcomp ~events-date-separator (&key date-str)
|
||||
(div :class "pt-2 pb-1"
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
(defcomp ~events-sentinel (&key page next-url)
|
||||
(div :id (str "sentinel-" page) :class "h-4 opacity-0 pointer-events-none"
|
||||
:hx-get next-url :hx-trigger "intersect once delay:250ms" :hx-swap "outerHTML"
|
||||
:sx-get next-url :sx-trigger "intersect once delay:250ms" :sx-swap "outerHTML"
|
||||
:role "status" :aria-hidden "true"
|
||||
(div :class "text-center text-xs text-stone-400" "loading...")))
|
||||
|
||||
@@ -80,24 +80,24 @@
|
||||
|
||||
(defcomp ~events-view-toggle (&key list-href tile-href hx-select list-active tile-active list-svg tile-svg)
|
||||
(div :class "hidden md:flex justify-end px-3 pt-3 gap-1"
|
||||
(a :href list-href :hx-get list-href :hx-target "#main-panel" :hx-select hx-select
|
||||
:hx-swap "outerHTML" :hx-push-url "true"
|
||||
(a :href list-href :sx-get list-href :sx-target "#main-panel" :sx-select hx-select
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class (str "p-1.5 rounded " list-active) :title "List view"
|
||||
:_ "on click js localStorage.removeItem('events_view') end"
|
||||
(raw! list-svg))
|
||||
(a :href tile-href :hx-get tile-href :hx-target "#main-panel" :hx-select hx-select
|
||||
:hx-swap "outerHTML" :hx-push-url "true"
|
||||
list-svg)
|
||||
(a :href tile-href :sx-get tile-href :sx-target "#main-panel" :sx-select hx-select
|
||||
:sx-swap "outerHTML" :sx-push-url "true"
|
||||
:class (str "p-1.5 rounded " tile-active) :title "Tile view"
|
||||
:_ "on click js localStorage.setItem('events_view','tile') end"
|
||||
(raw! tile-svg))))
|
||||
tile-svg)))
|
||||
|
||||
(defcomp ~events-grid (&key grid-cls cards-html)
|
||||
(div :class grid-cls (raw! cards-html)))
|
||||
(defcomp ~events-grid (&key grid-cls cards)
|
||||
(div :class grid-cls cards))
|
||||
|
||||
(defcomp ~events-empty ()
|
||||
(div :class "px-3 py-12 text-center text-stone-400"
|
||||
(i :class "fa fa-calendar-xmark text-4xl mb-3" :aria-hidden "true")
|
||||
(p :class "text-lg" "No upcoming events")))
|
||||
|
||||
(defcomp ~events-main-panel-body (&key toggle-html body-html)
|
||||
(<> (raw! toggle-html) (raw! body-html) (div :class "pb-8")))
|
||||
(defcomp ~events-main-panel-body (&key toggle body)
|
||||
(<> toggle body (div :class "pb-8")))
|
||||
|
||||
501
events/sexp/forms.sexpr
Normal file
501
events/sexp/forms.sexpr
Normal file
@@ -0,0 +1,501 @@
|
||||
;; Events form components — entry edit, slot add/edit, entry add,
|
||||
;; ticket type add/edit, add buttons, post search results.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Slot picker option (shared by entry-edit and entry-add)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-slot-option (&key value data-start data-end data-flexible data-cost selected label)
|
||||
(option :value value :data-start data-start :data-end data-end
|
||||
:data-flexible data-flexible :data-cost data-cost
|
||||
:selected selected
|
||||
label))
|
||||
|
||||
(defcomp ~events-slot-picker (&key id options)
|
||||
(select :id id :name "slot_id" :class "w-full border p-2 rounded"
|
||||
:data-slot-picker "" :required "required"
|
||||
options))
|
||||
|
||||
(defcomp ~events-no-slots ()
|
||||
(div :class "text-sm text-stone-500" "No slots defined for this day."))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Entry edit form (_types/entry/_edit.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-entry-edit-form (&key entry-id list-container put-url cancel-url csrf
|
||||
name-val slot-picker
|
||||
start-val end-val cost-display
|
||||
ticket-price-val ticket-count-val
|
||||
action-btn cancel-btn)
|
||||
(section :id (str "entry-" entry-id) :class list-container
|
||||
(div :id (str "entry-errors-" entry-id) :class "mt-2 text-sm text-red-600")
|
||||
(form :class "space-y-3 mt-4" :sx-put put-url
|
||||
:sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
|
||||
;; Name
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "entry-name-" entry-id) "Name")
|
||||
(input :id (str "entry-name-" entry-id) :name "name"
|
||||
:class "w-full border p-2 rounded" :placeholder "Name"
|
||||
:value name-val))
|
||||
|
||||
;; Slot picker
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "entry-slot-" entry-id) "Slot")
|
||||
slot-picker)
|
||||
|
||||
;; Time inputs (flexible slots)
|
||||
(div :data-time-fields "" :class "hidden space-y-3"
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "entry-start-" entry-id) "From")
|
||||
(input :id (str "entry-start-" entry-id) :name "start_at" :type "time"
|
||||
:class "w-full border p-2 rounded" :value start-val
|
||||
:data-entry-start ""))
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "entry-end-" entry-id) "To")
|
||||
(input :id (str "entry-end-" entry-id) :name "end_at" :type "time"
|
||||
:class "w-full border p-2 rounded" :value end-val
|
||||
:data-entry-end ""))
|
||||
(p :class "text-xs text-stone-500" :data-slot-boundary ""))
|
||||
|
||||
;; Fixed time summary
|
||||
(div :data-fixed-summary "" :class "hidden text-sm text-stone-600")
|
||||
|
||||
;; Cost display
|
||||
(div :data-cost-row "" :class "hidden text-sm font-medium text-stone-700"
|
||||
"Estimated Cost: "
|
||||
(span :data-cost-display "" :class "text-green-600" cost-display))
|
||||
|
||||
;; Ticket Configuration
|
||||
(div :class "border-t pt-3 mt-3"
|
||||
(h4 :class "text-sm font-semibold text-stone-700 mb-3" "Ticket Configuration")
|
||||
(div :class "grid grid-cols-2 gap-3"
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "entry-ticket-price-" entry-id)
|
||||
"Ticket Price (£)")
|
||||
(input :id (str "entry-ticket-price-" entry-id) :name "ticket_price"
|
||||
:type "number" :step "0.01" :min "0"
|
||||
:class "w-full border p-2 rounded"
|
||||
:placeholder "Leave empty for no tickets"
|
||||
:value ticket-price-val)
|
||||
(p :class "text-xs text-stone-500 mt-1" "Leave empty if no tickets needed"))
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "entry-ticket-count-" entry-id) "Total Tickets")
|
||||
(input :id (str "entry-ticket-count-" entry-id) :name "ticket_count"
|
||||
:type "number" :min "0"
|
||||
:class "w-full border p-2 rounded"
|
||||
:placeholder "Leave empty for unlimited"
|
||||
:value ticket-count-val)
|
||||
(p :class "text-xs text-stone-500 mt-1" "Leave empty for unlimited"))))
|
||||
|
||||
;; Buttons
|
||||
(div :class "flex justify-end gap-2 pt-2"
|
||||
(button :type "button" :class cancel-btn
|
||||
:sx-get cancel-url
|
||||
:sx-target (str "#entry-" entry-id) :sx-swap "outerHTML"
|
||||
"Cancel")
|
||||
(button :type "submit" :class action-btn
|
||||
:data-confirm "true" :data-confirm-title "Save entry?"
|
||||
:data-confirm-text "Are you sure you want to save this entry?"
|
||||
:data-confirm-icon "question"
|
||||
:data-confirm-confirm-text "Yes, save it"
|
||||
:data-confirm-cancel-text "Cancel"
|
||||
(i :class "fa fa-save") " Save entry")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Post search results (_types/entry/_post_search_results.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-post-search-item (&key post-url entry-id csrf post-id
|
||||
img title)
|
||||
(form :sx-post post-url :sx-target (str "#entry-posts-" entry-id) :sx-swap "innerHTML"
|
||||
:class "p-2 hover:bg-stone-50 cursor-pointer rounded text-sm border-b"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "post_id" :value post-id)
|
||||
(button :type "submit" :class "w-full text-left flex items-center gap-2"
|
||||
:data-confirm "" :data-confirm-title "Add post?"
|
||||
:data-confirm-text (str "Add " title " to this entry?")
|
||||
:data-confirm-icon "question"
|
||||
:data-confirm-confirm-text "Yes, add it"
|
||||
:data-confirm-cancel-text "Cancel"
|
||||
img (span title))))
|
||||
|
||||
(defcomp ~events-post-search-sentinel (&key page next-url)
|
||||
(div :id (str "post-search-sentinel-" page)
|
||||
:sx-get next-url
|
||||
:sx-trigger "intersect once delay:250ms, sentinel:retry"
|
||||
:sx-swap "outerHTML"
|
||||
:_ "
|
||||
init
|
||||
if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end
|
||||
|
||||
on sentinel:retry
|
||||
remove .hidden from .js-loading in me
|
||||
add .hidden to .js-neterr in me
|
||||
set me.style.pointerEvents to 'none'
|
||||
set me.style.opacity to '0'
|
||||
trigger htmx:consume on me
|
||||
call htmx.trigger(me, 'intersect')
|
||||
end
|
||||
|
||||
def backoff()
|
||||
add .hidden to .js-loading in me
|
||||
remove .hidden from .js-neterr in me
|
||||
set myMs to Number(me.dataset.retryMs)
|
||||
if myMs < 10000 then set me.dataset.retryMs to myMs * 2 end
|
||||
js setTimeout(() => htmx.trigger(me, 'sentinel:retry'), myMs)
|
||||
end
|
||||
|
||||
on htmx:beforeRequest
|
||||
set me.style.pointerEvents to 'none'
|
||||
set me.style.opacity to '0'
|
||||
end
|
||||
|
||||
on htmx:afterSwap
|
||||
set me.dataset.retryMs to 1000
|
||||
end
|
||||
|
||||
on htmx:sendError call backoff()
|
||||
on htmx:responseError call backoff()
|
||||
on htmx:timeout call backoff()
|
||||
"
|
||||
:role "status" :aria-live "polite" :aria-hidden "true" :class "py-2"
|
||||
(div :class "text-xs text-center text-stone-400 js-loading" "Loading more...")
|
||||
(div :class "text-xs text-center text-stone-400 js-neterr hidden" "Connection error. Retrying...")))
|
||||
|
||||
(defcomp ~events-post-search-end ()
|
||||
(div :class "py-2 text-xs text-center text-stone-400" "End of results"))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Slot edit form (_types/slot/_edit.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-day-checkbox (&key name label checked)
|
||||
(label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-100"
|
||||
(input :type "checkbox" :name name :value "1" :data-day name :checked checked)
|
||||
(span label)))
|
||||
|
||||
(defcomp ~events-day-all-checkbox (&key checked)
|
||||
(label :class "flex items-center gap-1 px-2 py-1 rounded-full bg-slate-200"
|
||||
(input :type "checkbox" :data-day-all "" :checked checked)
|
||||
(span "All")))
|
||||
|
||||
(defcomp ~events-slot-edit-form (&key slot-id list-container put-url cancel-url csrf
|
||||
name-val cost-val start-val end-val desc-val
|
||||
days flexible-checked
|
||||
action-btn cancel-btn)
|
||||
(section :id (str "slot-" slot-id) :class list-container
|
||||
(div :id "slot-errors" :class "mt-2 text-sm text-red-600")
|
||||
(form :class "space-y-3 mt-4" :sx-put put-url
|
||||
:sx-target (str "#slot-" slot-id) :sx-swap "outerHTML"
|
||||
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
|
||||
;; Name
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "slot-name-" slot-id) "Name")
|
||||
(input :id (str "slot-name-" slot-id) :name "name" :placeholder "Name"
|
||||
:class "w-full border p-2 rounded" :value name-val))
|
||||
|
||||
;; Cost
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "slot-cost-" slot-id) "Cost")
|
||||
(input :id (str "slot-cost-" slot-id) :name "cost" :placeholder "Cost e.g. 12.50"
|
||||
:class "w-full border p-2 rounded" :value cost-val))
|
||||
|
||||
;; Start time
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "slot-start-" slot-id) "Start time")
|
||||
(input :id (str "slot-start-" slot-id) :name "time_start" :placeholder "Start HH:MM"
|
||||
:class "w-full border p-2 rounded" :value start-val))
|
||||
|
||||
;; End time
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "slot-end-" slot-id) "End time")
|
||||
(input :id (str "slot-end-" slot-id) :name "time_end" :placeholder "End HH:MM"
|
||||
:class "w-full border p-2 rounded" :value end-val))
|
||||
|
||||
;; Description
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "slot-desc-" slot-id) "Description")
|
||||
(textarea :id (str "slot-desc-" slot-id) :name "description" :rows "2"
|
||||
:placeholder "Description" :class "w-full border p-2 rounded"
|
||||
desc-val))
|
||||
|
||||
;; Days
|
||||
(div
|
||||
(span :class "block text-sm font-medium text-stone-700 mb-1" "Days")
|
||||
(div :class "flex flex-wrap gap-3 items-center text-sm" :data-days-group ""
|
||||
days))
|
||||
|
||||
;; Flexible
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "slot-flexible-" slot-id) "Flexible booking")
|
||||
(label :class "inline-flex items-center gap-2 text-xs"
|
||||
(input :id (str "slot-flexible-" slot-id) :type "checkbox" :name "flexible"
|
||||
:value "1" :checked flexible-checked)
|
||||
(span "Allow bookings at any time within this band")))
|
||||
|
||||
;; Buttons
|
||||
(div :class "flex justify-end gap-2 pt-2"
|
||||
(button :type "button" :class cancel-btn
|
||||
:sx-get cancel-url
|
||||
:sx-target (str "#slot-" slot-id) :sx-swap "outerHTML"
|
||||
"Cancel")
|
||||
(button :type "submit" :class action-btn
|
||||
:data-confirm "true" :data-confirm-title "Save slot?"
|
||||
:data-confirm-text "Are you sure you want to save this slot?"
|
||||
:data-confirm-icon "question"
|
||||
:data-confirm-confirm-text "Yes, save it"
|
||||
:data-confirm-cancel-text "Cancel"
|
||||
(i :class "fa fa-save") " Save slot")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Slot add form (_types/slots/_add.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-slot-add-form (&key post-url csrf days action-btn cancel-btn cancel-url)
|
||||
(form :sx-post post-url :sx-target "#slots-table" :sx-select "#slots-table"
|
||||
:sx-disinherit "sx-select" :sx-swap "outerHTML"
|
||||
:sx-headers csrf :class "space-y-3"
|
||||
(div :class "grid grid-cols-1 md:grid-cols-4 gap-3"
|
||||
(div :class "md:col-span-2"
|
||||
(label :class "block text-xs font-semibold mb-1" "Name")
|
||||
(input :type "text" :name "name" :class "w-full border rounded px-2 py-1 text-sm" :required "required"))
|
||||
(div :class "md:col-span-2"
|
||||
(label :class "block text-xs font-semibold mb-1" "Description")
|
||||
(input :type "text" :name "description" :class "w-full border rounded px-2 py-1 text-sm"))
|
||||
(div
|
||||
(label :class "block text-xs font-semibold mb-1" "Days")
|
||||
(div :class "flex flex-wrap gap-1 text-xs" :data-days-group ""
|
||||
days))
|
||||
(div
|
||||
(label :class "block text-xs font-semibold mb-1" "Time start")
|
||||
(input :type "time" :name "time_start" :class "w-full border rounded px-2 py-1 text-sm" :required "required"))
|
||||
(div
|
||||
(label :class "block text-xs font-semibold mb-1" "Time end")
|
||||
(input :type "time" :name "time_end" :class "w-full border rounded px-2 py-1 text-sm" :required "required"))
|
||||
(div
|
||||
(label :class "block text-xs font-semibold mb-1" "Cost")
|
||||
(input :type "text" :name "cost" :class "w-full border rounded px-2 py-1 text-sm" :placeholder "e.g. 5.00"))
|
||||
(div :class "md:col-span-2"
|
||||
(label :class "block text-xs font-semibold mb-1" "Flexible booking")
|
||||
(label :class "inline-flex items-center gap-2 text-xs"
|
||||
(input :type "checkbox" :name "flexible" :value "1")
|
||||
(span "Allow bookings at any time within this band"))))
|
||||
(div :class "flex justify-end gap-2 pt-2"
|
||||
(button :type "button" :class cancel-btn
|
||||
:sx-get cancel-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
|
||||
"Cancel")
|
||||
(button :type "submit" :class action-btn
|
||||
:data-confirm "true" :data-confirm-title "Add slot?"
|
||||
:data-confirm-text "Are you sure you want to add this slot?"
|
||||
:data-confirm-icon "question"
|
||||
:data-confirm-confirm-text "Yes, add it"
|
||||
:data-confirm-cancel-text "Cancel"
|
||||
(i :class "fa fa-save") " Save slot"))))
|
||||
|
||||
(defcomp ~events-slot-add-button (&key pre-action add-url)
|
||||
(button :type "button" :class pre-action
|
||||
:sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
|
||||
"+ Add slot"))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Entry add form (_types/day/_add.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-entry-add-form (&key post-url csrf slot-picker
|
||||
action-btn cancel-btn cancel-url)
|
||||
(<>
|
||||
(div :id "entry-errors" :class "mt-2 text-sm text-red-600")
|
||||
(form :class "mt-4 grid grid-cols-1 md:grid-cols-4 gap-2"
|
||||
:sx-post post-url :sx-target "#day-entries"
|
||||
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
|
||||
:sx-swap "innerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
|
||||
;; Entry name
|
||||
(input :name "name" :type "text" :required "required"
|
||||
:class "border rounded px-3 py-2" :placeholder "Entry name")
|
||||
|
||||
;; Slot picker
|
||||
slot-picker
|
||||
|
||||
;; Time entry + cost display
|
||||
(div :class "md:col-span-2 flex flex-col gap-2"
|
||||
;; Time inputs (flexible)
|
||||
(div :data-time-fields "" :class "hidden"
|
||||
(div :class "mb-2"
|
||||
(label :class "block text-xs font-medium text-stone-700 mb-1" "From")
|
||||
(input :name "start_time" :type "time" :class "border rounded px-3 py-2 w-full"
|
||||
:data-entry-start ""))
|
||||
(div :class "mb-2"
|
||||
(label :class "block text-xs font-medium text-stone-700 mb-1" "To")
|
||||
(input :name "end_time" :type "time" :class "border rounded px-3 py-2 w-full"
|
||||
:data-entry-end ""))
|
||||
(p :class "text-xs text-stone-500" :data-slot-boundary ""))
|
||||
;; Cost display
|
||||
(div :data-cost-row "" :class "hidden text-sm font-medium text-stone-700"
|
||||
"Estimated Cost: "
|
||||
(span :data-cost-display "" :class "text-green-600" "£0.00"))
|
||||
;; Fixed summary
|
||||
(div :data-fixed-summary "" :class "hidden text-sm text-stone-600"))
|
||||
|
||||
;; Ticket Configuration
|
||||
(div :class "md:col-span-4 border-t pt-3 mt-2"
|
||||
(h4 :class "text-sm font-semibold text-stone-700 mb-3" "Ticket Configuration (Optional)")
|
||||
(div :class "grid grid-cols-2 gap-3"
|
||||
(div
|
||||
(label :class "block text-xs font-medium text-stone-700 mb-1"
|
||||
"Ticket Price (£)")
|
||||
(input :name "ticket_price" :type "number" :step "0.01" :min "0"
|
||||
:class "w-full border rounded px-3 py-2 text-sm"
|
||||
:placeholder "Leave empty for no tickets"))
|
||||
(div
|
||||
(label :class "block text-xs font-medium text-stone-700 mb-1" "Total Tickets")
|
||||
(input :name "ticket_count" :type "number" :min "0"
|
||||
:class "w-full border rounded px-3 py-2 text-sm"
|
||||
:placeholder "Leave empty for unlimited"))))
|
||||
|
||||
;; Buttons
|
||||
(div :class "flex justify-end gap-2 pt-2 md:col-span-4"
|
||||
(button :type "button" :class cancel-btn
|
||||
:sx-get cancel-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
|
||||
"Cancel")
|
||||
(button :type "submit" :class action-btn
|
||||
:data-confirm "true" :data-confirm-title "Add entry?"
|
||||
:data-confirm-text "Are you sure you want to add this entry?"
|
||||
:data-confirm-icon "question"
|
||||
:data-confirm-confirm-text "Yes, add it"
|
||||
:data-confirm-cancel-text "Cancel"
|
||||
(i :class "fa fa-save") " Save entry")))))
|
||||
|
||||
(defcomp ~events-entry-add-button (&key pre-action add-url)
|
||||
(button :type "button" :class pre-action
|
||||
:sx-get add-url :sx-target "#entry-add-container" :sx-swap "innerHTML"
|
||||
"+ Add entry"))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Ticket type edit form (_types/ticket_type/_edit.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-ticket-type-edit-form (&key ticket-id list-container put-url cancel-url csrf
|
||||
name-val cost-val count-val
|
||||
action-btn cancel-btn)
|
||||
(section :id (str "ticket-" ticket-id) :class list-container
|
||||
(div :id "ticket-errors" :class "mt-2 text-sm text-red-600")
|
||||
(form :class "space-y-3 mt-4" :sx-put put-url
|
||||
:sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML"
|
||||
:sx-on:afterRequest "if (event.detail.successful) this.reset()"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
|
||||
;; Name
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "ticket-name-" ticket-id) "Name")
|
||||
(input :id (str "ticket-name-" ticket-id) :name "name"
|
||||
:placeholder "e.g. Adult, Child, Student"
|
||||
:class "w-full border p-2 rounded" :value name-val :required "required"))
|
||||
|
||||
;; Cost
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "ticket-cost-" ticket-id) "Cost (£)")
|
||||
(input :id (str "ticket-cost-" ticket-id) :name "cost" :type "number"
|
||||
:step "0.01" :min "0" :placeholder "e.g. 5.00"
|
||||
:class "w-full border p-2 rounded" :value cost-val :required "required"))
|
||||
|
||||
;; Count
|
||||
(div
|
||||
(label :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
:for (str "ticket-count-" ticket-id) "Count")
|
||||
(input :id (str "ticket-count-" ticket-id) :name "count" :type "number"
|
||||
:min "0" :placeholder "e.g. 50"
|
||||
:class "w-full border p-2 rounded" :value count-val :required "required"))
|
||||
|
||||
;; Buttons
|
||||
(div :class "flex justify-end gap-2 pt-2"
|
||||
(button :type "button" :class cancel-btn
|
||||
:sx-get cancel-url
|
||||
:sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML"
|
||||
"Cancel")
|
||||
(button :type "submit" :class action-btn
|
||||
:data-confirm "true" :data-confirm-title "Save ticket type?"
|
||||
:data-confirm-text "Are you sure you want to save this ticket type?"
|
||||
:data-confirm-icon "question"
|
||||
:data-confirm-confirm-text "Yes, save it"
|
||||
:data-confirm-cancel-text "Cancel"
|
||||
(i :class "fa fa-save") " Save ticket type")))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Ticket type add form (_types/ticket_types/_add.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-ticket-type-add-form (&key post-url csrf action-btn cancel-btn cancel-url)
|
||||
(form :sx-post post-url :sx-target "#tickets-table" :sx-select "#tickets-table"
|
||||
:sx-disinherit "sx-select" :sx-swap "outerHTML"
|
||||
:sx-headers csrf :class "space-y-3"
|
||||
(div :class "grid grid-cols-1 md:grid-cols-3 gap-3"
|
||||
(div
|
||||
(label :class "block text-xs font-semibold mb-1" "Name")
|
||||
(input :type "text" :name "name" :class "w-full border rounded px-2 py-1 text-sm"
|
||||
:placeholder "e.g. Adult, Child, Student" :required "required"))
|
||||
(div
|
||||
(label :class "block text-xs font-semibold mb-1" "Cost (£)")
|
||||
(input :type "number" :name "cost" :step "0.01" :min "0"
|
||||
:class "w-full border rounded px-2 py-1 text-sm"
|
||||
:placeholder "e.g. 5.00" :required "required"))
|
||||
(div
|
||||
(label :class "block text-xs font-semibold mb-1" "Count")
|
||||
(input :type "number" :name "count" :min "0"
|
||||
:class "w-full border rounded px-2 py-1 text-sm"
|
||||
:placeholder "e.g. 50" :required "required")))
|
||||
(div :class "flex justify-end gap-2 pt-2"
|
||||
(button :type "button" :class cancel-btn
|
||||
:sx-get cancel-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
|
||||
"Cancel")
|
||||
(button :type "submit" :class action-btn
|
||||
:data-confirm "true" :data-confirm-title "Add ticket type?"
|
||||
:data-confirm-text "Are you sure you want to add this ticket type?"
|
||||
:data-confirm-icon "question"
|
||||
:data-confirm-confirm-text "Yes, add it"
|
||||
:data-confirm-cancel-text "Cancel"
|
||||
(i :class "fa fa-save") " Save ticket type"))))
|
||||
|
||||
(defcomp ~events-ticket-type-add-button (&key action-btn add-url)
|
||||
(button :class action-btn
|
||||
:sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
|
||||
(i :class "fa fa-plus") " Add ticket type"))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Entry admin nav — placeholder
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-admin-placeholder-nav ()
|
||||
(div :class "relative nav-group"
|
||||
(span :class "block px-3 py-2 text-stone-400 text-sm italic" "Admin options")))
|
||||
|
||||
;; Entry admin main panel — ticket_types link
|
||||
(defcomp ~events-entry-admin-main-panel (&key link)
|
||||
link)
|
||||
94
events/sexp/fragments.sexpr
Normal file
94
events/sexp/fragments.sexpr
Normal file
@@ -0,0 +1,94 @@
|
||||
;; Events fragment components — served as HTML fragments for other apps.
|
||||
;; container-cards entries, account page tickets, account page bookings.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Container cards entries (fragments/container_cards_entries.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-frag-entry-card (&key href name date-str time-str)
|
||||
(a :href href
|
||||
:class "flex flex-col gap-1 px-3 py-2 bg-stone-50 hover:bg-stone-100 rounded border border-stone-200 transition text-sm whitespace-nowrap flex-shrink-0 min-w-[180px]"
|
||||
(div :class "font-medium text-stone-900 truncate" name)
|
||||
(div :class "text-xs text-stone-600" date-str)
|
||||
(div :class "text-xs text-stone-500" time-str)))
|
||||
|
||||
(defcomp ~events-frag-entries-widget (&key cards)
|
||||
(div :class "mt-4 mb-2"
|
||||
(h3 :class "text-sm font-semibold text-stone-700 mb-2 px-2" "Events:")
|
||||
(div :class "overflow-x-auto scrollbar-hide" :style "scroll-behavior: smooth;"
|
||||
(div :class "flex gap-2 px-2" cards))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Account page tickets (fragments/account_page_tickets.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-frag-ticket-badge (&key state)
|
||||
(cond
|
||||
((= state "checked_in")
|
||||
(span :class "inline-flex items-center rounded-full bg-blue-50 border border-blue-200 px-2.5 py-0.5 text-xs font-medium text-blue-700" "checked in"))
|
||||
((= state "confirmed")
|
||||
(span :class "inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700" "confirmed"))
|
||||
(true
|
||||
(span :class "inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700" state))))
|
||||
|
||||
(defcomp ~events-frag-ticket-item (&key href entry-name date-str calendar-name type-name badge)
|
||||
(div :class "py-4 first:pt-0 last:pb-0"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "min-w-0 flex-1"
|
||||
(a :href href :class "text-sm font-medium text-stone-800 hover:text-emerald-700 transition"
|
||||
entry-name)
|
||||
(div :class "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500"
|
||||
(span date-str)
|
||||
calendar-name
|
||||
type-name))
|
||||
(div :class "flex-shrink-0" badge))))
|
||||
|
||||
(defcomp ~events-frag-tickets-panel (&key items)
|
||||
(div :class "w-full max-w-3xl mx-auto px-4 py-6"
|
||||
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
|
||||
(h1 :class "text-xl font-semibold tracking-tight" "Tickets")
|
||||
items)))
|
||||
|
||||
(defcomp ~events-frag-tickets-empty ()
|
||||
(p :class "text-sm text-stone-500" "No tickets yet."))
|
||||
|
||||
(defcomp ~events-frag-tickets-list (&key items)
|
||||
(div :class "divide-y divide-stone-100" items))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Account page bookings (fragments/account_page_bookings.html)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~events-frag-booking-badge (&key state)
|
||||
(cond
|
||||
((= state "confirmed")
|
||||
(span :class "inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700" "confirmed"))
|
||||
((= state "provisional")
|
||||
(span :class "inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700" "provisional"))
|
||||
(true
|
||||
(span :class "inline-flex items-center rounded-full bg-stone-50 border border-stone-200 px-2.5 py-0.5 text-xs font-medium text-stone-600" state))))
|
||||
|
||||
(defcomp ~events-frag-booking-item (&key name date-str calendar-name cost-str badge)
|
||||
(div :class "py-4 first:pt-0 last:pb-0"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "min-w-0 flex-1"
|
||||
(p :class "text-sm font-medium text-stone-800" name)
|
||||
(div :class "mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500"
|
||||
(span date-str)
|
||||
calendar-name
|
||||
cost-str))
|
||||
(div :class "flex-shrink-0" badge))))
|
||||
|
||||
(defcomp ~events-frag-bookings-panel (&key items)
|
||||
(div :class "w-full max-w-3xl mx-auto px-4 py-6"
|
||||
(div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"
|
||||
(h1 :class "text-xl font-semibold tracking-tight" "Bookings")
|
||||
items)))
|
||||
|
||||
(defcomp ~events-frag-bookings-empty ()
|
||||
(p :class "text-sm text-stone-500" "No bookings yet."))
|
||||
|
||||
(defcomp ~events-frag-bookings-list (&key items)
|
||||
(div :class "divide-y divide-stone-100" items))
|
||||
@@ -20,7 +20,7 @@
|
||||
(i :class "fa fa-calendar-day")
|
||||
(span date-str)))
|
||||
|
||||
(defcomp ~events-entry-label (&key entry-id title-html times-html)
|
||||
(defcomp ~events-entry-label (&key entry-id title times)
|
||||
(div :id (str "entry-title-" entry-id) :class "flex gap-1 items-center"
|
||||
(raw! title-html) (raw! times-html)))
|
||||
title times))
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
(defcomp ~events-markets-create-form (&key create-url csrf)
|
||||
(<>
|
||||
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
|
||||
(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;"
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-on:beforeRequest "document.querySelector('#market-create-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#market-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "flex-1"
|
||||
(label :class "block text-sm text-gray-600" "Name")
|
||||
@@ -14,10 +14,10 @@
|
||||
:placeholder "e.g. Farm Shop, Bakery"))
|
||||
(button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
|
||||
|
||||
(defcomp ~events-markets-panel (&key form-html list-html)
|
||||
(defcomp ~events-markets-panel (&key form list)
|
||||
(section :class "p-4"
|
||||
(raw! form-html)
|
||||
(div :id "markets-list" :class "mt-6" (raw! list-html))))
|
||||
form
|
||||
(div :id "markets-list" :class "mt-6" list)))
|
||||
|
||||
(defcomp ~events-markets-empty ()
|
||||
(p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
|
||||
@@ -33,7 +33,7 @@
|
||||
: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 del-url :hx-trigger "confirmed"
|
||||
:hx-target "#markets-list" :hx-select "#markets-list" :hx-swap "outerHTML"
|
||||
:hx-headers csrf-hdr
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
;; Events page-level components (slots, ticket types, buy form, cart, posts nav)
|
||||
|
||||
(defcomp ~events-slot-days-pills (&key days-inner-html)
|
||||
(div :class "flex flex-wrap gap-1" (raw! days-inner-html)))
|
||||
(defcomp ~events-slot-days-pills (&key days-inner)
|
||||
(div :class "flex flex-wrap gap-1" days-inner))
|
||||
|
||||
(defcomp ~events-slot-day-pill (&key day)
|
||||
(span :class "px-2 py-0.5 rounded-full text-xs bg-slate-200" day))
|
||||
@@ -9,11 +9,11 @@
|
||||
(defcomp ~events-slot-no-days ()
|
||||
(span :class "text-xs text-slate-400" "No days"))
|
||||
|
||||
(defcomp ~events-slot-panel (&key slot-id list-container days-html flexible time-str cost-str pre-action edit-url)
|
||||
(defcomp ~events-slot-panel (&key slot-id list-container days flexible time-str cost-str pre-action edit-url)
|
||||
(section :id (str "slot-" slot-id) :class list-container
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Days")
|
||||
(div :class "mt-1" (raw! days-html)))
|
||||
(div :class "mt-1" days))
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Flexible")
|
||||
(div :class "mt-1" flexible))
|
||||
@@ -24,11 +24,11 @@
|
||||
(div :class "flex flex-col"
|
||||
(div :class "text-xs font-semibold uppercase tracking-wide text-stone-500" "Cost")
|
||||
(div :class "mt-1" cost-str)))
|
||||
(button :type "button" :class pre-action :hx-get edit-url
|
||||
:hx-target (str "#slot-" slot-id) :hx-swap "outerHTML" "Edit")))
|
||||
(button :type "button" :class pre-action :sx-get edit-url
|
||||
:sx-target (str "#slot-" slot-id) :sx-swap "outerHTML" "Edit")))
|
||||
|
||||
(defcomp ~events-slot-description-oob (&key description)
|
||||
(div :id "slot-description-title" :hx-swap-oob "outerHTML"
|
||||
(div :id "slot-description-title" :sx-swap-oob "outerHTML"
|
||||
:class "text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
description))
|
||||
|
||||
@@ -36,15 +36,15 @@
|
||||
(tr (td :colspan "5" :class "p-3 text-stone-500" "No slots yet.")))
|
||||
|
||||
(defcomp ~events-slots-row (&key tr-cls slot-href pill-cls hx-select slot-name description
|
||||
flexible days-html time-str cost-str action-btn del-url csrf-hdr)
|
||||
flexible days time-str cost-str action-btn del-url csrf-hdr)
|
||||
(tr :class tr-cls
|
||||
(td :class "p-2 align-top w-1/6"
|
||||
(div :class "font-medium"
|
||||
(a :href slot-href :class pill-cls :hx-get slot-href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true" slot-name))
|
||||
(a :href slot-href :class pill-cls :sx-get slot-href :sx-target "#main-panel"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" slot-name))
|
||||
(p :class "text-stone-500 whitespace-pre-line break-all w-full" description))
|
||||
(td :class "p-2 align-top w-1/6" flexible)
|
||||
(td :class "p-2 align-top w-1/6" (raw! days-html))
|
||||
(td :class "p-2 align-top w-1/6" days)
|
||||
(td :class "p-2 align-top w-1/6" time-str)
|
||||
(td :class "p-2 align-top w-1/6" cost-str)
|
||||
(td :class "p-2 align-top w-1/6"
|
||||
@@ -53,11 +53,11 @@
|
||||
:data-confirm-text "This action cannot be undone."
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:hx-delete del-url :hx-target "#slots-table" :hx-select "#slots-table"
|
||||
:hx-swap "outerHTML" :hx-headers csrf-hdr :hx-trigger "confirmed"
|
||||
:sx-delete del-url :sx-target "#slots-table" :sx-select "#slots-table"
|
||||
:sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed"
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
(defcomp ~events-slots-table (&key list-container rows-html pre-action add-url)
|
||||
(defcomp ~events-slots-table (&key list-container rows pre-action add-url)
|
||||
(section :id "slots-table" :class list-container
|
||||
(table :class "w-full text-sm border table-fixed"
|
||||
(thead :class "bg-stone-100"
|
||||
@@ -67,10 +67,10 @@
|
||||
(th :class "text-left p-2 w-1/6" "Time")
|
||||
(th :class "text-left p-2 w-1/6" "Cost")
|
||||
(th :class "text-left p-2 w-1/6" "Actions")))
|
||||
(tbody (raw! rows-html)))
|
||||
(tbody rows))
|
||||
(div :id "slot-add-container" :class "mt-4"
|
||||
(button :type "button" :class pre-action
|
||||
:hx-get add-url :hx-target "#slot-add-container" :hx-swap "innerHTML"
|
||||
:sx-get add-url :sx-target "#slot-add-container" :sx-swap "innerHTML"
|
||||
"+ Add slot"))))
|
||||
|
||||
(defcomp ~events-ticket-type-col (&key label value)
|
||||
@@ -81,9 +81,9 @@
|
||||
(defcomp ~events-ticket-type-panel (&key ticket-id list-container c1 c2 c3 pre-action edit-url)
|
||||
(section :id (str "ticket-" ticket-id) :class list-container
|
||||
(div :class "grid grid-cols-1 sm:grid-cols-3 gap-4 text-sm"
|
||||
(raw! c1) (raw! c2) (raw! c3))
|
||||
(button :type "button" :class pre-action :hx-get edit-url
|
||||
:hx-target (str "#ticket-" ticket-id) :hx-swap "outerHTML" "Edit")))
|
||||
c1 c2 c3)
|
||||
(button :type "button" :class pre-action :sx-get edit-url
|
||||
:sx-target (str "#ticket-" ticket-id) :sx-swap "outerHTML" "Edit")))
|
||||
|
||||
(defcomp ~events-ticket-types-empty-row ()
|
||||
(tr (td :colspan "4" :class "p-3 text-stone-500" "No ticket types yet.")))
|
||||
@@ -93,8 +93,8 @@
|
||||
(tr :class tr-cls
|
||||
(td :class "p-2 align-top w-1/3"
|
||||
(div :class "font-medium"
|
||||
(a :href tt-href :class pill-cls :hx-get tt-href :hx-target "#main-panel"
|
||||
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true" tt-name)))
|
||||
(a :href tt-href :class pill-cls :sx-get tt-href :sx-target "#main-panel"
|
||||
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true" tt-name)))
|
||||
(td :class "p-2 align-top w-1/4" cost-str)
|
||||
(td :class "p-2 align-top w-1/4" count)
|
||||
(td :class "p-2 align-top w-1/6"
|
||||
@@ -103,11 +103,11 @@
|
||||
:data-confirm-text "This action cannot be undone."
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, delete it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:hx-delete del-url :hx-target "#tickets-table" :hx-select "#tickets-table"
|
||||
:hx-swap "outerHTML" :hx-headers csrf-hdr :hx-trigger "confirmed"
|
||||
:sx-delete del-url :sx-target "#tickets-table" :sx-select "#tickets-table"
|
||||
:sx-swap "outerHTML" :sx-headers csrf-hdr :sx-trigger "confirmed"
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
(defcomp ~events-ticket-types-table (&key list-container rows-html action-btn add-url)
|
||||
(defcomp ~events-ticket-types-table (&key list-container rows action-btn add-url)
|
||||
(section :id "tickets-table" :class list-container
|
||||
(table :class "w-full text-sm border table-fixed"
|
||||
(thead :class "bg-stone-100"
|
||||
@@ -115,16 +115,16 @@
|
||||
(th :class "text-left p-2 w-1/4" "Cost")
|
||||
(th :class "text-left p-2 w-1/4" "Count")
|
||||
(th :class "text-left p-2 w-1/6" "Actions")))
|
||||
(tbody (raw! rows-html)))
|
||||
(tbody rows))
|
||||
(div :id "ticket-add-container" :class "mt-4"
|
||||
(button :class action-btn :hx-get add-url :hx-target "#ticket-add-container" :hx-swap "innerHTML"
|
||||
(button :class action-btn :sx-get add-url :sx-target "#ticket-add-container" :sx-swap "innerHTML"
|
||||
(i :class "fa fa-plus") " Add ticket type"))))
|
||||
|
||||
(defcomp ~events-ticket-config-display (&key price-str count-str show-js)
|
||||
(div :class "space-y-2"
|
||||
(div :class "flex items-center gap-2"
|
||||
(span :class "text-sm font-medium text-stone-700" "Price:")
|
||||
(span :class "font-medium text-green-600" (raw! price-str)))
|
||||
(span :class "font-medium text-green-600" price-str))
|
||||
(div :class "flex items-center gap-2"
|
||||
(span :class "text-sm font-medium text-stone-700" "Available:")
|
||||
(span :class "font-medium text-blue-600" count-str))
|
||||
@@ -139,10 +139,10 @@
|
||||
|
||||
(defcomp ~events-ticket-config-form (&key entry-id hidden-cls update-url csrf price-val count-val hide-js)
|
||||
(form :id (str "ticket-form-" entry-id) :class (str hidden-cls " space-y-3 mt-2 p-3 border rounded bg-stone-50")
|
||||
:hx-post update-url :hx-target (str "#entry-tickets-" entry-id) :hx-swap "innerHTML"
|
||||
:sx-post update-url :sx-target (str "#entry-tickets-" entry-id) :sx-swap "innerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div (label :for (str "ticket-price-" entry-id) :class "block text-sm font-medium text-stone-700 mb-1"
|
||||
(raw! "Ticket Price (£)"))
|
||||
"Ticket Price (£)")
|
||||
(input :type "number" :id (str "ticket-price-" entry-id) :name "ticket_price"
|
||||
:step "0.01" :min "0" :value price-val
|
||||
:class "w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@@ -174,37 +174,37 @@
|
||||
(i :class "fa fa-shopping-cart text-[0.6rem]" :aria-hidden "true")
|
||||
(str " " count " in basket")))
|
||||
|
||||
(defcomp ~events-buy-info-bar (&key items-html)
|
||||
(div :class "flex items-center gap-3 mb-3 text-xs text-stone-500" (raw! items-html)))
|
||||
(defcomp ~events-buy-info-bar (&key items)
|
||||
(div :class "flex items-center gap-3 mb-3 text-xs text-stone-500" items))
|
||||
|
||||
(defcomp ~events-buy-type-item (&key type-name cost-str adjust-controls-html)
|
||||
(defcomp ~events-buy-type-item (&key type-name cost-str adjust-controls)
|
||||
(div :class "flex items-center justify-between p-3 rounded-lg bg-stone-50 border border-stone-100"
|
||||
(div (div :class "font-medium text-sm" type-name)
|
||||
(div :class "text-xs text-stone-500" cost-str))
|
||||
(raw! adjust-controls-html)))
|
||||
adjust-controls))
|
||||
|
||||
(defcomp ~events-buy-types-wrapper (&key items-html)
|
||||
(div :class "space-y-2" (raw! items-html)))
|
||||
(defcomp ~events-buy-types-wrapper (&key items)
|
||||
(div :class "space-y-2" items))
|
||||
|
||||
(defcomp ~events-buy-default (&key price-str adjust-controls-html)
|
||||
(defcomp ~events-buy-default (&key price-str adjust-controls)
|
||||
(<> (div :class "flex items-center justify-between mb-4"
|
||||
(div (span :class "font-medium text-green-600" price-str)
|
||||
(span :class "text-sm text-stone-500 ml-2" "per ticket")))
|
||||
(raw! adjust-controls-html)))
|
||||
adjust-controls))
|
||||
|
||||
(defcomp ~events-buy-panel (&key entry-id info-html body-html)
|
||||
(defcomp ~events-buy-panel (&key entry-id info body)
|
||||
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-stone-200 bg-white p-4"
|
||||
(h3 :class "text-sm font-semibold text-stone-700 mb-3"
|
||||
(i :class "fa fa-ticket mr-1" :aria-hidden "true") "Tickets")
|
||||
(raw! info-html) (raw! body-html)))
|
||||
info body))
|
||||
|
||||
(defcomp ~events-adjust-form (&key adjust-url target extra-cls csrf entry-id tt-html count-val btn-html)
|
||||
(form :hx-post adjust-url :hx-target target :hx-swap "outerHTML" :class extra-cls
|
||||
(defcomp ~events-adjust-form (&key adjust-url target extra-cls csrf entry-id tt count-val btn)
|
||||
(form :sx-post adjust-url :sx-target target :sx-swap "outerHTML" :class extra-cls
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "entry_id" :value entry-id)
|
||||
(raw! tt-html)
|
||||
tt
|
||||
(input :type "hidden" :name "count" :value count-val)
|
||||
(raw! btn-html)))
|
||||
btn))
|
||||
|
||||
(defcomp ~events-adjust-tt-hidden (&key ticket-type-id)
|
||||
(input :type "hidden" :name "ticket_type_id" :value ticket-type-id))
|
||||
@@ -231,16 +231,16 @@
|
||||
(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" count)))))
|
||||
|
||||
(defcomp ~events-adjust-controls (&key minus-html cart-icon-html plus-html)
|
||||
(div :class "flex items-center gap-2" (raw! minus-html) (raw! cart-icon-html) (raw! plus-html)))
|
||||
(defcomp ~events-adjust-controls (&key minus cart-icon plus)
|
||||
(div :class "flex items-center gap-2" minus cart-icon plus))
|
||||
|
||||
(defcomp ~events-buy-result (&key entry-id count-label tickets-html remaining-html my-tickets-href)
|
||||
(defcomp ~events-buy-result (&key entry-id count-label tickets remaining my-tickets-href)
|
||||
(div :id (str "ticket-buy-" entry-id) :class "rounded-xl border border-emerald-200 bg-emerald-50 p-4"
|
||||
(div :class "flex items-center gap-2 mb-3"
|
||||
(i :class "fa fa-check-circle text-emerald-600" :aria-hidden "true")
|
||||
(span :class "font-semibold text-emerald-800" count-label))
|
||||
(div :class "space-y-2 mb-4" (raw! tickets-html))
|
||||
(raw! remaining-html)
|
||||
(div :class "space-y-2 mb-4" tickets)
|
||||
remaining
|
||||
(div :class "mt-3 flex gap-2"
|
||||
(a :href my-tickets-href :class "text-sm text-emerald-700 hover:text-emerald-900 underline"
|
||||
"View all my tickets"))))
|
||||
@@ -256,25 +256,25 @@
|
||||
(p :class "text-xs text-stone-500" text))
|
||||
|
||||
(defcomp ~events-cart-icon-logo (&key blog-href logo)
|
||||
(div :id "cart-mini" :hx-swap-oob "true"
|
||||
(div :id "cart-mini" :sx-swap-oob "true"
|
||||
(div :class "h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0"
|
||||
(a :href blog-href :class "h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1"
|
||||
(img :src logo :class "h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0")))))
|
||||
|
||||
(defcomp ~events-cart-icon-badge (&key cart-href count)
|
||||
(div :id "cart-mini" :hx-swap-oob "true"
|
||||
(div :id "cart-mini" :sx-swap-oob "true"
|
||||
(a :href cart-href :class "relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700"
|
||||
(i :class "fa fa-shopping-cart text-5xl" :aria-hidden "true")
|
||||
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
|
||||
count))))
|
||||
|
||||
;; Inline ticket widget (for all-events/page-summary cards)
|
||||
(defcomp ~events-tw-form (&key ticket-url target csrf entry-id count-val btn-html)
|
||||
(form :action ticket-url :method "post" :hx-post ticket-url :hx-target target :hx-swap "outerHTML"
|
||||
(defcomp ~events-tw-form (&key ticket-url target csrf entry-id count-val btn)
|
||||
(form :action ticket-url :method "post" :sx-post ticket-url :sx-target target :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(input :type "hidden" :name "entry_id" :value entry-id)
|
||||
(input :type "hidden" :name "count" :value count-val)
|
||||
(raw! btn-html)))
|
||||
btn))
|
||||
|
||||
(defcomp ~events-tw-cart-plus ()
|
||||
(button :type "submit" :class "relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1"
|
||||
@@ -293,40 +293,40 @@
|
||||
(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)))))
|
||||
|
||||
(defcomp ~events-tw-widget (&key entry-id price-html inner-html)
|
||||
(defcomp ~events-tw-widget (&key entry-id price inner)
|
||||
(div :id (str "page-ticket-" entry-id) :class "flex items-center gap-2"
|
||||
(span :class "text-green-600 font-medium text-sm" (raw! price-html))
|
||||
(raw! inner-html)))
|
||||
(span :class "text-green-600 font-medium text-sm" price)
|
||||
inner))
|
||||
|
||||
;; Entry posts panel
|
||||
(defcomp ~events-entry-posts-panel (&key posts-html search-url entry-id)
|
||||
(defcomp ~events-entry-posts-panel (&key posts search-url entry-id)
|
||||
(div :class "space-y-2"
|
||||
(raw! posts-html)
|
||||
posts
|
||||
(div :class "mt-3 pt-3 border-t"
|
||||
(label :class "block text-xs font-medium text-stone-700 mb-1" "Add Post")
|
||||
(input :type "text" :placeholder "Search posts..."
|
||||
:class "w-full px-3 py-2 border rounded text-sm"
|
||||
:hx-get search-url :hx-trigger "keyup changed delay:300ms, load"
|
||||
:hx-target (str "#post-search-results-" entry-id) :hx-swap "innerHTML" :name "q")
|
||||
:sx-get search-url :sx-trigger "keyup changed delay:300ms, load"
|
||||
:sx-target (str "#post-search-results-" entry-id) :sx-swap "innerHTML" :name "q")
|
||||
(div :id (str "post-search-results-" entry-id) :class "mt-2 max-h-96 overflow-y-auto border rounded"))))
|
||||
|
||||
(defcomp ~events-entry-posts-list (&key items-html)
|
||||
(div :class "space-y-2" (raw! items-html)))
|
||||
(defcomp ~events-entry-posts-list (&key items)
|
||||
(div :class "space-y-2" items))
|
||||
|
||||
(defcomp ~events-entry-posts-none ()
|
||||
(p :class "text-sm text-stone-400" "No posts associated"))
|
||||
|
||||
(defcomp ~events-entry-post-item (&key img-html title del-url entry-id csrf-hdr)
|
||||
(defcomp ~events-entry-post-item (&key img title del-url entry-id csrf-hdr)
|
||||
(div :class "flex items-center justify-between gap-3 p-2 bg-stone-50 rounded border"
|
||||
(raw! img-html) (span :class "text-sm flex-1" title)
|
||||
img (span :class "text-sm flex-1" title)
|
||||
(button :type "button" :class "text-xs text-red-600 hover:text-red-800 flex-shrink-0"
|
||||
:data-confirm "true" :data-confirm-title "Remove post?"
|
||||
:data-confirm-text (str "This will remove " title " from this entry")
|
||||
:data-confirm-icon "warning" :data-confirm-confirm-text "Yes, remove it"
|
||||
:data-confirm-cancel-text "Cancel" :data-confirm-event "confirmed"
|
||||
:hx-delete del-url :hx-trigger "confirmed"
|
||||
:hx-target (str "#entry-posts-" entry-id) :hx-swap "innerHTML"
|
||||
:hx-headers csrf-hdr
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target (str "#entry-posts-" entry-id) :sx-swap "innerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa fa-times") " Remove")))
|
||||
|
||||
(defcomp ~events-post-img (&key src alt)
|
||||
@@ -337,19 +337,19 @@
|
||||
|
||||
;; Entry posts nav OOB
|
||||
(defcomp ~events-entry-posts-nav-oob-empty ()
|
||||
(div :id "entry-posts-nav-wrapper" :hx-swap-oob "true"))
|
||||
(div :id "entry-posts-nav-wrapper" :sx-swap-oob "true"))
|
||||
|
||||
(defcomp ~events-entry-posts-nav-oob (&key items-html)
|
||||
(defcomp ~events-entry-posts-nav-oob (&key items)
|
||||
(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 "entry-posts-nav-wrapper" :hx-swap-oob "true"
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" (raw! items-html))))
|
||||
:id "entry-posts-nav-wrapper" :sx-swap-oob "true"
|
||||
(div :class "flex overflow-x-auto gap-1 scrollbar-thin" items)))
|
||||
|
||||
(defcomp ~events-entry-nav-post (&key href nav-btn img-html title)
|
||||
(a :href href :class nav-btn (raw! img-html) (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
|
||||
(defcomp ~events-entry-nav-post (&key href nav-btn img title)
|
||||
(a :href href :class nav-btn img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
|
||||
|
||||
;; Post nav entries OOB
|
||||
(defcomp ~events-post-nav-oob-empty ()
|
||||
(div :id "entries-calendars-nav-wrapper" :hx-swap-oob "true"))
|
||||
(div :id "entries-calendars-nav-wrapper" :sx-swap-oob "true"))
|
||||
|
||||
(defcomp ~events-post-nav-entry (&key href nav-btn name time-str)
|
||||
(a :href href :class nav-btn
|
||||
@@ -363,9 +363,9 @@
|
||||
(i :class "fa fa-calendar" :aria-hidden "true")
|
||||
(div name)))
|
||||
|
||||
(defcomp ~events-post-nav-wrapper (&key items-html hyperscript)
|
||||
(defcomp ~events-post-nav-wrapper (&key items hyperscript)
|
||||
(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 "entries-calendars-nav-wrapper" :hx-swap-oob "true"
|
||||
:id "entries-calendars-nav-wrapper" :sx-swap-oob "true"
|
||||
(button :class "entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
:aria-label "Scroll left"
|
||||
:_ "on click set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft - 200"
|
||||
@@ -373,7 +373,7 @@
|
||||
(div :id "associated-items-container"
|
||||
:class "overflow-y-auto sm:overflow-x-auto sm:overflow-y-visible scrollbar-hide max-h-[50vh] sm:max-h-none"
|
||||
:style "scroll-behavior: smooth;" :_ hyperscript
|
||||
(div :class "flex flex-col sm:flex-row gap-1" (raw! items-html)))
|
||||
(div :class "flex flex-col sm:flex-row gap-1" items))
|
||||
(style ".scrollbar-hide::-webkit-scrollbar { display: none; } .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; }")
|
||||
(button :class "entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
:aria-label "Scroll right"
|
||||
@@ -381,6 +381,6 @@
|
||||
(i :class "fa fa-chevron-right"))))
|
||||
|
||||
;; Entry nav post link (with image)
|
||||
(defcomp ~events-entry-nav-post-link (&key href img-html title)
|
||||
(defcomp ~events-entry-nav-post-link (&key href img title)
|
||||
(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"
|
||||
(raw! img-html) (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
|
||||
img (div :class "flex-1 min-w-0" (div :class "font-medium truncate" title))))
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
(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 update-url :hx-target "#payments-panel" :hx-swap "outerHTML" :hx-select "#payments-panel" :class "space-y-3"
|
||||
(form :sx-put update-url :sx-target "#payments-panel" :sx-swap "outerHTML" :sx-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 merchant-code :placeholder "e.g. ME4J6100" :class input-cls))
|
||||
@@ -23,10 +23,10 @@
|
||||
(defcomp ~events-markets-create-form (&key create-url csrf)
|
||||
(<>
|
||||
(div :id "market-create-errors" :class "mt-2 text-sm text-red-600")
|
||||
(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;"
|
||||
(form :class "mt-4 flex gap-2 items-end" :sx-post create-url
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-on:beforeRequest "document.querySelector('#market-create-errors').textContent='';"
|
||||
:sx-on:responseError "document.querySelector('#market-create-errors').textContent='Error'; if(event.detail.response){event.detail.response.clone().text().then(function(t){event.target.closest('form').querySelector('[id$=errors]').innerHTML=t})}"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(div :class "flex-1"
|
||||
(label :class "block text-sm text-gray-600" "Name")
|
||||
@@ -34,10 +34,10 @@
|
||||
:placeholder "e.g. Farm Shop, Bakery"))
|
||||
(button :type "submit" :class "border rounded px-3 py-2" "Add market"))))
|
||||
|
||||
(defcomp ~events-markets-panel (&key form-html list-html)
|
||||
(defcomp ~events-markets-panel (&key form list)
|
||||
(section :class "p-4"
|
||||
(raw! form-html)
|
||||
(div :id "markets-list" :class "mt-6" (raw! list-html))))
|
||||
form
|
||||
(div :id "markets-list" :class "mt-6" list)))
|
||||
|
||||
(defcomp ~events-markets-empty ()
|
||||
(p :class "text-gray-500 mt-4" "No markets yet. Create one above."))
|
||||
@@ -53,7 +53,7 @@
|
||||
: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 del-url :hx-trigger "confirmed"
|
||||
:hx-target "#markets-list" :hx-select "#markets-list" :hx-swap "outerHTML"
|
||||
:hx-headers csrf-hdr
|
||||
:sx-delete del-url :sx-trigger "confirmed"
|
||||
:sx-target "#markets-list" :sx-select "#markets-list" :sx-swap "outerHTML"
|
||||
:sx-headers csrf-hdr
|
||||
(i :class "fa-solid fa-trash")))))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
;; Events ticket components
|
||||
|
||||
(defcomp ~events-ticket-card (&key href entry-name type-name time-str cal-name badge-html code-prefix)
|
||||
(defcomp ~events-ticket-card (&key href entry-name type-name time-str cal-name badge code-prefix)
|
||||
(a :href href :class "block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "flex-1 min-w-0"
|
||||
@@ -9,20 +9,20 @@
|
||||
(when time-str (div :class "text-sm text-stone-500 mt-1" time-str))
|
||||
(when cal-name (div :class "text-xs text-stone-400 mt-0.5" cal-name)))
|
||||
(div :class "flex flex-col items-end gap-1 flex-shrink-0"
|
||||
(raw! badge-html)
|
||||
badge
|
||||
(span :class "text-xs text-stone-400 font-mono" (str code-prefix "..."))))))
|
||||
|
||||
(defcomp ~events-tickets-panel (&key list-container has-tickets cards-html)
|
||||
(defcomp ~events-tickets-panel (&key list-container has-tickets cards)
|
||||
(section :id "tickets-list" :class list-container
|
||||
(h1 :class "text-2xl font-bold mb-6" "My Tickets")
|
||||
(if has-tickets
|
||||
(div :class "space-y-4" (raw! cards-html))
|
||||
(div :class "space-y-4" cards)
|
||||
(div :class "text-center py-12 text-stone-500"
|
||||
(i :class "fa fa-ticket text-4xl mb-4 block" :aria-hidden "true")
|
||||
(p :class "text-lg" "No tickets yet")
|
||||
(p :class "text-sm mt-1" "Tickets will appear here after you purchase them.")))))
|
||||
|
||||
(defcomp ~events-ticket-detail (&key list-container back-href header-bg entry-name badge-html
|
||||
(defcomp ~events-ticket-detail (&key list-container back-href header-bg entry-name badge
|
||||
type-name code time-date time-range cal-name
|
||||
type-desc checkin-str qr-script)
|
||||
(section :id "ticket-detail" :class (str list-container " max-w-lg mx-auto")
|
||||
@@ -32,7 +32,7 @@
|
||||
(div :class (str "px-6 py-4 border-b border-stone-100 " header-bg)
|
||||
(div :class "flex items-center justify-between"
|
||||
(h1 :class "text-xl font-bold" entry-name)
|
||||
(raw! badge-html))
|
||||
badge)
|
||||
(when type-name (div :class "text-sm text-stone-600 mt-1" type-name)))
|
||||
(div :class "px-6 py-8 flex flex-col items-center border-b border-stone-100"
|
||||
(div :id (str "ticket-qr-" code) :class "bg-white p-4 rounded-lg border border-stone-200")
|
||||
@@ -63,7 +63,7 @@
|
||||
(div :class "text-xs text-stone-500" date-str))
|
||||
|
||||
(defcomp ~events-ticket-admin-checkin-form (&key checkin-url code csrf)
|
||||
(form :hx-post checkin-url :hx-target (str "#ticket-row-" code) :hx-swap "outerHTML"
|
||||
(form :sx-post checkin-url :sx-target (str "#ticket-row-" code) :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700 transition"
|
||||
(i :class "fa fa-check mr-1" :aria-hidden "true") "Check in")))
|
||||
@@ -72,18 +72,18 @@
|
||||
(span :class "text-xs text-blue-600"
|
||||
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))
|
||||
|
||||
(defcomp ~events-ticket-admin-row (&key code code-short entry-name date-html type-name badge-html action-html)
|
||||
(defcomp ~events-ticket-admin-row (&key code code-short entry-name date type-name badge action)
|
||||
(tr :class "hover:bg-stone-50 transition" :id (str "ticket-row-" code)
|
||||
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) (raw! date-html))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
|
||||
(td :class "px-4 py-3 text-sm" type-name)
|
||||
(td :class "px-4 py-3" (raw! badge-html))
|
||||
(td :class "px-4 py-3" (raw! action-html))))
|
||||
(td :class "px-4 py-3" badge)
|
||||
(td :class "px-4 py-3" action)))
|
||||
|
||||
(defcomp ~events-ticket-admin-panel (&key list-container stats-html lookup-url has-tickets rows-html)
|
||||
(defcomp ~events-ticket-admin-panel (&key list-container stats lookup-url has-tickets rows)
|
||||
(section :id "ticket-admin" :class list-container
|
||||
(h1 :class "text-2xl font-bold mb-6" "Ticket Admin")
|
||||
(div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" (raw! stats-html))
|
||||
(div :class "grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8" stats)
|
||||
(div :class "rounded-xl border border-stone-200 bg-white p-6 mb-8"
|
||||
(h2 :class "text-lg font-semibold mb-4"
|
||||
(i :class "fa fa-qrcode mr-2" :aria-hidden "true") "Scan / Look Up Ticket")
|
||||
@@ -91,8 +91,8 @@
|
||||
(input :type "text" :id "ticket-code-input" :name "code"
|
||||
:placeholder "Enter or scan ticket code..."
|
||||
:class "flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
:hx-get lookup-url :hx-trigger "keyup changed delay:300ms"
|
||||
:hx-target "#lookup-result" :hx-include "this" :autofocus "true")
|
||||
:sx-get lookup-url :sx-trigger "keyup changed delay:300ms"
|
||||
:sx-target "#lookup-result" :sx-include "this" :autofocus "true")
|
||||
(button :type "button"
|
||||
:class "px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition"
|
||||
:onclick "document.getElementById('ticket-code-input').dispatchEvent(new Event('keyup'))"
|
||||
@@ -110,19 +110,19 @@
|
||||
(th :class "px-4 py-3 text-left font-medium text-stone-600" "Type")
|
||||
(th :class "px-4 py-3 text-left font-medium text-stone-600" "State")
|
||||
(th :class "px-4 py-3 text-left font-medium text-stone-600" "Actions")))
|
||||
(tbody :class "divide-y divide-stone-100" (raw! rows-html))))
|
||||
(div :class "px-6 py-8 text-center text-stone-500" "No tickets yet")))))
|
||||
(tbody :class "divide-y divide-stone-100" rows))
|
||||
(div :class "px-6 py-8 text-center text-stone-500" "No tickets yet"))))))
|
||||
|
||||
(defcomp ~events-checkin-error (&key message)
|
||||
(div :class "rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-800"
|
||||
(i :class "fa fa-exclamation-circle mr-2" :aria-hidden "true") message))
|
||||
|
||||
(defcomp ~events-checkin-success-row (&key code code-short entry-name date-html type-name badge-html time-str)
|
||||
(defcomp ~events-checkin-success-row (&key code code-short entry-name date type-name badge time-str)
|
||||
(tr :class "bg-blue-50" :id (str "ticket-row-" code)
|
||||
(td :class "px-4 py-3" (span :class "font-mono text-xs" code-short))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) (raw! date-html))
|
||||
(td :class "px-4 py-3" (div :class "font-medium" entry-name) date)
|
||||
(td :class "px-4 py-3 text-sm" type-name)
|
||||
(td :class "px-4 py-3" (raw! badge-html))
|
||||
(td :class "px-4 py-3" badge)
|
||||
(td :class "px-4 py-3"
|
||||
(span :class "text-xs text-blue-600"
|
||||
(i :class "fa fa-check-circle" :aria-hidden "true") (str " " time-str)))))
|
||||
@@ -143,14 +143,14 @@
|
||||
(defcomp ~events-lookup-cal (&key cal-name)
|
||||
(div :class "text-xs text-stone-400 mt-0.5" cal-name))
|
||||
|
||||
(defcomp ~events-lookup-status (&key badge-html code)
|
||||
(div :class "mt-2" (raw! badge-html) (span :class "text-xs text-stone-400 ml-2 font-mono" code)))
|
||||
(defcomp ~events-lookup-status (&key badge code)
|
||||
(div :class "mt-2" badge (span :class "text-xs text-stone-400 ml-2 font-mono" code)))
|
||||
|
||||
(defcomp ~events-lookup-checkin-time (&key date-str)
|
||||
(div :class "text-xs text-blue-600 mt-1" (str "Checked in: " date-str)))
|
||||
|
||||
(defcomp ~events-lookup-checkin-btn (&key checkin-url code csrf)
|
||||
(form :hx-post checkin-url :hx-target (str "#checkin-action-" code) :hx-swap "innerHTML"
|
||||
(form :sx-post checkin-url :sx-target (str "#checkin-action-" code) :sx-swap "innerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type "submit"
|
||||
:class "px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold text-lg"
|
||||
@@ -166,26 +166,26 @@
|
||||
(i :class "fa fa-times-circle text-3xl" :aria-hidden "true")
|
||||
(div :class "text-sm font-medium mt-1" "Cancelled")))
|
||||
|
||||
(defcomp ~events-lookup-card (&key info-html code action-html)
|
||||
(defcomp ~events-lookup-card (&key info code action)
|
||||
(div :class "rounded-lg border border-stone-200 bg-stone-50 p-4"
|
||||
(div :class "flex items-start justify-between gap-4"
|
||||
(div :class "flex-1" (raw! info-html))
|
||||
(div :id (str "checkin-action-" code) (raw! action-html)))))
|
||||
(div :class "flex-1" info)
|
||||
(div :id (str "checkin-action-" code) action))))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-row (&key code code-short type-name badge-html action-html)
|
||||
(defcomp ~events-entry-tickets-admin-row (&key code code-short type-name badge action)
|
||||
(tr :class "hover:bg-stone-50" :id (str "entry-ticket-row-" code)
|
||||
(td :class "px-4 py-2 font-mono text-xs" code-short)
|
||||
(td :class "px-4 py-2" type-name)
|
||||
(td :class "px-4 py-2" (raw! badge-html))
|
||||
(td :class "px-4 py-2" (raw! action-html))))
|
||||
(td :class "px-4 py-2" badge)
|
||||
(td :class "px-4 py-2" action)))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-checkin (&key checkin-url code csrf)
|
||||
(form :hx-post checkin-url :hx-target (str "#entry-ticket-row-" code) :hx-swap "outerHTML"
|
||||
(form :sx-post checkin-url :sx-target (str "#entry-ticket-row-" code) :sx-swap "outerHTML"
|
||||
(input :type "hidden" :name "csrf_token" :value csrf)
|
||||
(button :type "submit" :class "px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
|
||||
"Check in")))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-table (&key rows-html)
|
||||
(defcomp ~events-entry-tickets-admin-table (&key rows)
|
||||
(div :class "overflow-x-auto rounded-xl border border-stone-200"
|
||||
(table :class "w-full text-sm"
|
||||
(thead :class "bg-stone-50"
|
||||
@@ -193,14 +193,14 @@
|
||||
(th :class "px-4 py-2 text-left font-medium text-stone-600" "Type")
|
||||
(th :class "px-4 py-2 text-left font-medium text-stone-600" "State")
|
||||
(th :class "px-4 py-2 text-left font-medium text-stone-600" "Actions")))
|
||||
(tbody :class "divide-y divide-stone-100" (raw! rows-html)))))
|
||||
(tbody :class "divide-y divide-stone-100" rows))))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-empty ()
|
||||
(div :class "text-center py-6 text-stone-500 text-sm" "No tickets for this entry"))
|
||||
|
||||
(defcomp ~events-entry-tickets-admin-panel (&key entry-name count-label body-html)
|
||||
(defcomp ~events-entry-tickets-admin-panel (&key entry-name count-label body)
|
||||
(div :class "space-y-4"
|
||||
(div :class "flex items-center justify-between"
|
||||
(h3 :class "text-lg font-semibold" (str "Tickets for: " entry-name))
|
||||
(span :class "text-sm text-stone-500" count-label))
|
||||
(raw! body-html)))
|
||||
body))
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<div
|
||||
id="sentinel-{{ page }}"
|
||||
class="h-4 opacity-0 pointer-events-none"
|
||||
hx-get="{{ entries_url }}"
|
||||
hx-trigger="intersect once delay:250ms"
|
||||
hx-swap="outerHTML"
|
||||
sx-get="{{ entries_url }}"
|
||||
sx-trigger="intersect once delay:250ms"
|
||||
sx-swap="outerHTML"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
{% set tile_href = (current_local_href ~ {'view': 'tile'}|qs)|host %}
|
||||
<a
|
||||
href="{{ list_href }}"
|
||||
hx-get="{{ list_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ list_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' }}"
|
||||
title="List view"
|
||||
_="on click js localStorage.removeItem('events_view') end"
|
||||
onclick="localStorage.removeItem('events_view')"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
@@ -19,14 +19,14 @@
|
||||
</a>
|
||||
<a
|
||||
href="{{ tile_href }}"
|
||||
hx-get="{{ tile_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ tile_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' }}"
|
||||
title="Tile view"
|
||||
_="on click js localStorage.setItem('events_view','tile') end"
|
||||
onclick="localStorage.setItem('events_view','tile')"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
id="calendar-description-title"
|
||||
{% if oob %}
|
||||
hx-swap-oob="outerHTML"
|
||||
sx-swap-oob="outerHTML"
|
||||
{% endif %}
|
||||
class="text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
>
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
calendar_slug=calendar.slug,
|
||||
year=prev_year,
|
||||
month=month) }}"
|
||||
hx-get="{{ url_for('calendar.get',
|
||||
sx-get="{{ url_for('calendar.get',
|
||||
calendar_slug=calendar.slug,
|
||||
year=prev_year,
|
||||
month=month) }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
«
|
||||
</a>
|
||||
@@ -29,14 +29,14 @@
|
||||
calendar_slug=calendar.slug,
|
||||
year=prev_month_year,
|
||||
month=prev_month) }}"
|
||||
hx-get="{{ url_for('calendar.get',
|
||||
sx-get="{{ url_for('calendar.get',
|
||||
calendar_slug=calendar.slug,
|
||||
year=prev_month_year,
|
||||
month=prev_month) }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
‹
|
||||
</a>
|
||||
@@ -52,14 +52,14 @@
|
||||
calendar_slug=calendar.slug,
|
||||
year=next_month_year,
|
||||
month=next_month) }}"
|
||||
hx-get="{{ url_for('calendar.get',
|
||||
sx-get="{{ url_for('calendar.get',
|
||||
calendar_slug=calendar.slug,
|
||||
year=next_month_year,
|
||||
month=next_month) }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
›
|
||||
</a>
|
||||
@@ -71,14 +71,14 @@
|
||||
calendar_slug=calendar.slug,
|
||||
year=next_year,
|
||||
month=month) }}"
|
||||
hx-get="{{ url_for('calendar.get',
|
||||
sx-get="{{ url_for('calendar.get',
|
||||
calendar_slug=calendar.slug,
|
||||
year=next_year,
|
||||
month=month) }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
»
|
||||
</a>
|
||||
@@ -115,15 +115,15 @@
|
||||
year=day.date.year,
|
||||
month=day.date.month,
|
||||
day=day.date.day) }}"
|
||||
hx-get="{{ url_for('calendar.day.show_day',
|
||||
sx-get="{{ url_for('calendar.day.show_day',
|
||||
calendar_slug=calendar.slug,
|
||||
year=day.date.year,
|
||||
month=day.date.month,
|
||||
day=day.date.day) }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
{{ day.date.day }}
|
||||
</a>
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
<button
|
||||
type="button"
|
||||
class="mt-2 text-xs underline"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.admin.calendar_description_edit',
|
||||
calendar_slug=calendar.slug,
|
||||
) }}"
|
||||
hx-target="#calendar-description"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#calendar-description"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div id="calendar-description">
|
||||
<form
|
||||
hx-post="{{ url_for(
|
||||
sx-post="{{ url_for(
|
||||
'calendar.admin.calendar_description_save',
|
||||
calendar_slug=calendar.slug,
|
||||
) }}"
|
||||
hx-target="#calendar-description"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#calendar-description"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1 rounded border"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.admin.calendar_description_view',
|
||||
calendar_slug=calendar.slug,
|
||||
) }}"
|
||||
hx-target="#calendar-description"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#calendar-description"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
<form
|
||||
id="calendar-form"
|
||||
method="post"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
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()"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-on:beforeRequest="document.querySelector('#cal-put-errors').textContent='';"
|
||||
sx-on:responseError="if(event.detail.response){event.detail.response.clone().text().then(function(t){document.querySelector('#cal-put-errors').innerHTML=t})}"
|
||||
sx-on:afterRequest="if (event.detail.successful) this.reset()"
|
||||
|
||||
class="hidden space-y-4 mt-4"
|
||||
autocomplete="off"
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<a
|
||||
class="flex items-baseline gap-3"
|
||||
href="{{ calendar_href }}"
|
||||
hx-get="{{ calendar_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select ="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ calendar_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select ="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
<h3 class="font-semibold">{{ cal.name }}</h3>
|
||||
<h4 class="text-gray-500">/{{ cal.slug }}/</h4>
|
||||
@@ -27,12 +27,12 @@
|
||||
data-confirm-confirm-text="Yes, delete it"
|
||||
data-confirm-cancel-text="Cancel"
|
||||
data-confirm-event="confirmed"
|
||||
hx-delete="{{ url_for('calendar.delete', calendar_slug=cal.slug) }}"
|
||||
hx-trigger="confirmed"
|
||||
hx-target="#calendars-list"
|
||||
hx-select="#calendars-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
|
||||
sx-delete="{{ url_for('calendar.delete', calendar_slug=cal.slug) }}"
|
||||
sx-trigger="confirmed"
|
||||
sx-target="#calendars-list"
|
||||
sx-select="#calendars-list"
|
||||
sx-swap="outerHTML"
|
||||
sx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
<form
|
||||
class="mt-4 flex gap-2 items-end"
|
||||
hx-post="{{ url_for('calendars.create_calendar') }}"
|
||||
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;"
|
||||
sx-post="{{ url_for('calendars.create_calendar') }}"
|
||||
sx-target="#calendars-list"
|
||||
sx-select="#calendars-list"
|
||||
sx-swap="outerHTML"
|
||||
sx-on:beforeRequest="document.querySelector('#cal-create-errors').textContent='';"
|
||||
sx-on:responseError="if(event.detail.response){event.detail.response.clone().text().then(function(t){document.querySelector('#cal-create-errors').innerHTML=t})}"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
<form
|
||||
class="mt-4 grid grid-cols-1 md:grid-cols-4 gap-2"
|
||||
hx-post="{{ url_for(
|
||||
sx-post="{{ url_for(
|
||||
'calendar.day.calendar_entries.add_entry',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
month=month,
|
||||
year=year,
|
||||
) }}"
|
||||
hx-target="#day-entries"
|
||||
hx-on::after-request="if (event.detail.successful) this.reset()"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#day-entries"
|
||||
sx-on:afterRequest="if (event.detail.successful) this.reset()"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -123,14 +123,14 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.cancel_button}}"
|
||||
hx-get="{{ url_for('calendar.day.calendar_entries.add_button',
|
||||
sx-get="{{ url_for('calendar.day.calendar_entries.add_button',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
month=month,
|
||||
year=year,
|
||||
) }}"
|
||||
hx-target="#entry-add-container"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#entry-add-container"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.pre_action_button}}"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.day.calendar_entries.add_form',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
month=month,
|
||||
year=year,
|
||||
) }}"
|
||||
hx-target="#entry-add-container"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#entry-add-container"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
+ Add entry
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% if confirmed_entries %}
|
||||
<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"
|
||||
hx-swap-oob="true">
|
||||
sx-swap-oob="true">
|
||||
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
|
||||
{% call(entry) scrolling_menu('day-entries-container', confirmed_entries) %}
|
||||
<a
|
||||
@@ -29,5 +29,5 @@
|
||||
</div>
|
||||
{% else %}
|
||||
{# Empty placeholder to remove nav entries when none are confirmed #}
|
||||
<div id="day-entries-nav-wrapper" hx-swap-oob="true"></div>
|
||||
<div id="day-entries-nav-wrapper" sx-swap-oob="true"></div>
|
||||
{% endif %}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
<form
|
||||
class="space-y-3 mt-4"
|
||||
hx-put="{{ url_for(
|
||||
sx-put="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.put',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day, month=month, year=year,
|
||||
entry_id=entry.id
|
||||
) }}"
|
||||
hx-target="#entry-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#entry-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
@@ -161,14 +161,14 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{ styles.cancel_button }}"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.get',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day, month=month, year=year,
|
||||
entry_id=entry.id
|
||||
) }}"
|
||||
hx-target="#entry-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#entry-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.pre_action_button}}"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.get_edit',
|
||||
entry_id=entry.id,
|
||||
calendar_slug=calendar.slug,
|
||||
@@ -118,8 +118,8 @@
|
||||
month=month,
|
||||
year=year,
|
||||
) }}"
|
||||
hx-target="#entry-{{entry.id}}"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#entry-{{entry.id}}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
|
||||
{% include '_types/entry/_options.html' %}
|
||||
<div id="entry-title-{{entry.id}}" hx-swap-oob="innerHTML">
|
||||
<div id="entry-title-{{entry.id}}" sx-swap-oob="innerHTML">
|
||||
{% include '_types/entry/_title.html' %}
|
||||
</div>
|
||||
|
||||
<div id="entry-state-{{entry.id}}" hx-swap-oob="innerHTML">
|
||||
<div id="entry-state-{{entry.id}}" sx-swap-oob="innerHTML">
|
||||
{% include '_types/entry/_state.html' %}
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
<div id="calendar_entry_options_{{ entry.id }}" class="flex flex-col md:flex-row gap-1">
|
||||
{% if entry.state == 'provisional' %}
|
||||
<form
|
||||
hx-post="{{ url_for(
|
||||
sx-post="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.confirm_entry',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
@@ -9,9 +9,9 @@
|
||||
year=year,
|
||||
entry_id=entry.id
|
||||
) }}"
|
||||
hx-select="#calendar_entry_options_{{ entry.id }}"
|
||||
hx-target="#calendar_entry_options_{{entry.id}}"
|
||||
hx-swap="outerHTML"
|
||||
sx-select="#calendar_entry_options_{{ entry.id }}"
|
||||
sx-target="#calendar_entry_options_{{entry.id}}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button
|
||||
@@ -30,7 +30,7 @@
|
||||
</button>
|
||||
</form>
|
||||
<form
|
||||
hx-post="{{ url_for(
|
||||
sx-post="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.decline_entry',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
@@ -38,9 +38,9 @@
|
||||
year=year,
|
||||
entry_id=entry.id
|
||||
) }}"
|
||||
hx-select="#calendar_entry_options_{{ entry.id }}"
|
||||
hx-target="#calendar_entry_options_{{entry.id}}"
|
||||
hx-swap="outerHTML"
|
||||
sx-select="#calendar_entry_options_{{ entry.id }}"
|
||||
sx-target="#calendar_entry_options_{{entry.id}}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button
|
||||
@@ -61,7 +61,7 @@
|
||||
{% endif %}
|
||||
{% if entry.state == 'confirmed' %}
|
||||
<form
|
||||
hx-post="{{ url_for(
|
||||
sx-post="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.provisional_entry',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
@@ -69,10 +69,10 @@
|
||||
year=year,
|
||||
entry_id=entry.id
|
||||
) }}"
|
||||
hx-target="#calendar_entry_options_{{ entry.id }}"
|
||||
hx-select="#calendar_entry_options_{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="confirmed"
|
||||
sx-target="#calendar_entry_options_{{ entry.id }}"
|
||||
sx-select="#calendar_entry_options_{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-trigger="confirmed"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% for search_post in search_posts %}
|
||||
<form
|
||||
hx-post="{{ url_for(
|
||||
sx-post="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.add_post',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
@@ -8,8 +8,8 @@
|
||||
year=year,
|
||||
entry_id=entry.id
|
||||
) }}"
|
||||
hx-target="#entry-posts-{{entry.id}}"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#entry-posts-{{entry.id}}"
|
||||
sx-swap="innerHTML"
|
||||
class="p-2 hover:bg-stone-50 cursor-pointer rounded text-sm border-b"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
@@ -40,7 +40,7 @@
|
||||
{% if page < total_pages|int %}
|
||||
<div
|
||||
id="post-search-sentinel-{{ page }}"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.search_posts',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
@@ -50,42 +50,9 @@
|
||||
q=search_query,
|
||||
page=page + 1
|
||||
) }}"
|
||||
hx-trigger="intersect once delay:250ms, sentinel:retry"
|
||||
hx-swap="outerHTML"
|
||||
_="
|
||||
init
|
||||
if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end
|
||||
|
||||
on sentinel:retry
|
||||
remove .hidden from .js-loading in me
|
||||
add .hidden to .js-neterr in me
|
||||
set me.style.pointerEvents to 'none'
|
||||
set me.style.opacity to '0'
|
||||
trigger htmx:consume on me
|
||||
call htmx.trigger(me, 'intersect')
|
||||
end
|
||||
|
||||
def backoff()
|
||||
add .hidden to .js-loading in me
|
||||
remove .hidden from .js-neterr in me
|
||||
set myMs to Number(me.dataset.retryMs)
|
||||
if myMs < 10000 then set me.dataset.retryMs to myMs * 2 end
|
||||
js setTimeout(() => htmx.trigger(me, 'sentinel:retry'), myMs)
|
||||
end
|
||||
|
||||
on htmx:beforeRequest
|
||||
set me.style.pointerEvents to 'none'
|
||||
set me.style.opacity to '0'
|
||||
end
|
||||
|
||||
on htmx:afterSwap
|
||||
set me.dataset.retryMs to 1000
|
||||
end
|
||||
|
||||
on htmx:sendError call backoff()
|
||||
on htmx:responseError call backoff()
|
||||
on htmx:timeout call backoff()
|
||||
"
|
||||
sx-trigger="intersect once delay:250ms"
|
||||
sx-swap="outerHTML"
|
||||
sx-retry="exponential:1000:30000"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
data-confirm-confirm-text="Yes, remove it"
|
||||
data-confirm-cancel-text="Cancel"
|
||||
data-confirm-event="confirmed"
|
||||
hx-delete="{{ url_for(
|
||||
sx-delete="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.remove_post',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
@@ -31,10 +31,10 @@
|
||||
entry_id=entry.id,
|
||||
post_id=entry_post.id
|
||||
) }}"
|
||||
hx-trigger="confirmed"
|
||||
hx-target="#entry-posts-{{entry.id}}"
|
||||
hx-swap="innerHTML"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-trigger="confirmed"
|
||||
sx-target="#entry-posts-{{entry.id}}"
|
||||
sx-swap="innerHTML"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
>
|
||||
<i class="fa fa-times"></i> Remove
|
||||
</button>
|
||||
@@ -54,7 +54,7 @@
|
||||
type="text"
|
||||
placeholder="Search posts..."
|
||||
class="w-full px-3 py-2 border rounded text-sm"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.search_posts',
|
||||
calendar_slug=calendar.slug,
|
||||
day=day,
|
||||
@@ -62,9 +62,9 @@
|
||||
year=year,
|
||||
entry_id=entry.id
|
||||
) }}"
|
||||
hx-trigger="keyup changed delay:300ms, load"
|
||||
hx-target="#post-search-results-{{entry.id}}"
|
||||
hx-swap="innerHTML"
|
||||
sx-trigger="keyup changed delay:300ms, load"
|
||||
sx-target="#post-search-results-{{entry.id}}"
|
||||
sx-swap="innerHTML"
|
||||
name="q"
|
||||
/>
|
||||
<div id="post-search-results-{{entry.id}}" class="mt-2 max-h-96 overflow-y-auto border rounded"></div>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<form
|
||||
id="ticket-form-{{entry.id}}"
|
||||
class="{% if entry.ticket_price is not none %}hidden{% endif %} space-y-3 mt-2 p-3 border rounded bg-stone-50"
|
||||
hx-post="{{ url_for(
|
||||
sx-post="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.update_tickets',
|
||||
entry_id=entry.id,
|
||||
calendar_slug=calendar.slug,
|
||||
@@ -51,8 +51,8 @@
|
||||
month=month,
|
||||
year=year,
|
||||
) }}"
|
||||
hx-target="#entry-tickets-{{entry.id}}"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#entry-tickets-{{entry.id}}"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% if entry_posts %}
|
||||
<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="entry-posts-nav-wrapper"
|
||||
hx-swap-oob="true">
|
||||
sx-swap-oob="true">
|
||||
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
|
||||
{% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
|
||||
<a
|
||||
@@ -27,5 +27,5 @@
|
||||
</div>
|
||||
{% else %}
|
||||
{# Empty placeholder to remove nav posts when all are disassociated #}
|
||||
<div id="entry-posts-nav-wrapper" hx-swap-oob="true"></div>
|
||||
<div id="entry-posts-nav-wrapper" sx-swap-oob="true"></div>
|
||||
{% endif %}
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
<form
|
||||
class="mt-4 flex gap-2 items-end"
|
||||
hx-post="{{ url_for('markets.create_market') }}"
|
||||
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;"
|
||||
sx-post="{{ url_for('markets.create_market') }}"
|
||||
sx-target="#markets-list"
|
||||
sx-select="#markets-list"
|
||||
sx-swap="outerHTML"
|
||||
sx-on:beforeRequest="document.querySelector('#market-create-errors').textContent='';"
|
||||
sx-on:responseError="if(event.detail.response){event.detail.response.clone().text().then(function(t){document.querySelector('#market-create-errors').innerHTML=t})}"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="flex-1">
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
data-confirm-confirm-text="Yes, delete it"
|
||||
data-confirm-cancel-text="Cancel"
|
||||
data-confirm-event="confirmed"
|
||||
hx-delete="{{ url_for('markets.delete_market', market_slug=m.slug) }}"
|
||||
hx-trigger="confirmed"
|
||||
hx-target="#markets-list"
|
||||
hx-select="#markets-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
|
||||
sx-delete="{{ url_for('markets.delete_market', market_slug=m.slug) }}"
|
||||
sx-trigger="confirmed"
|
||||
sx-target="#markets-list"
|
||||
sx-select="#markets-list"
|
||||
sx-swap="outerHTML"
|
||||
sx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
<div
|
||||
id="sentinel-{{ page }}"
|
||||
class="h-4 opacity-0 pointer-events-none"
|
||||
hx-get="{{ entries_url }}"
|
||||
hx-trigger="intersect once delay:250ms"
|
||||
hx-swap="outerHTML"
|
||||
sx-get="{{ entries_url }}"
|
||||
sx-trigger="intersect once delay:250ms"
|
||||
sx-swap="outerHTML"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
{% set tile_href = (current_local_href ~ {'view': 'tile'}|qs)|host %}
|
||||
<a
|
||||
href="{{ list_href }}"
|
||||
hx-get="{{ list_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ list_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' }}"
|
||||
title="List view"
|
||||
_="on click js localStorage.removeItem('events_view') end"
|
||||
onclick="localStorage.removeItem('events_view')"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
||||
@@ -19,14 +19,14 @@
|
||||
</a>
|
||||
<a
|
||||
href="{{ tile_href }}"
|
||||
hx-get="{{ tile_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ tile_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' }}"
|
||||
title="Tile view"
|
||||
_="on click js localStorage.setItem('events_view','tile') end"
|
||||
onclick="localStorage.setItem('events_view','tile')"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<form
|
||||
action="{{ ticket_url }}"
|
||||
method="post"
|
||||
hx-post="{{ ticket_url }}"
|
||||
hx-target="#page-ticket-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ ticket_url }}"
|
||||
sx-target="#page-ticket-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
||||
@@ -26,9 +26,9 @@
|
||||
<form
|
||||
action="{{ ticket_url }}"
|
||||
method="post"
|
||||
hx-post="{{ ticket_url }}"
|
||||
hx-target="#page-ticket-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ ticket_url }}"
|
||||
sx-target="#page-ticket-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
||||
@@ -49,9 +49,9 @@
|
||||
<form
|
||||
action="{{ ticket_url }}"
|
||||
method="post"
|
||||
hx-post="{{ ticket_url }}"
|
||||
hx-target="#page-ticket-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ ticket_url }}"
|
||||
sx-target="#page-ticket-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="entry_id" value="{{ entry.id }}">
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
</p>
|
||||
|
||||
<form
|
||||
hx-put="{{ url_for('payments.update_sumup') }}"
|
||||
hx-target="#payments-panel"
|
||||
hx-swap="outerHTML"
|
||||
hx-select="#payments-panel"
|
||||
sx-put="{{ url_for('payments.update_sumup') }}"
|
||||
sx-target="#payments-panel"
|
||||
sx-swap="outerHTML"
|
||||
sx-select="#payments-panel"
|
||||
class="space-y-3"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
data-confirm-confirm-text="Yes, remove it"
|
||||
data-confirm-cancel-text="Cancel"
|
||||
data-confirm-event="confirmed"
|
||||
hx-post="{{ url_for('blog.post.admin.toggle_entry', slug=post.slug, entry_id=entry.id) }}"
|
||||
hx-trigger="confirmed"
|
||||
hx-target="#associated-entries-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
_="on htmx:afterRequest trigger entryToggled on body"
|
||||
sx-post="{{ url_for('blog.post.admin.toggle_entry', slug=post.slug, entry_id=entry.id) }}"
|
||||
sx-trigger="confirmed"
|
||||
sx-target="#associated-entries-list"
|
||||
sx-swap="outerHTML"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-on:afterSwap="document.body.dispatchEvent(new CustomEvent('entryToggled'))"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
{% if calendar.post.feature_image %}
|
||||
|
||||
@@ -8,14 +8,7 @@
|
||||
<h3 class="text-lg font-semibold">Browse Calendars</h3>
|
||||
{% for calendar in all_calendars %}
|
||||
<details class="border rounded-lg bg-white"
|
||||
_="on toggle
|
||||
if my.open
|
||||
for other in <details[open]/>
|
||||
if other is not me
|
||||
set other.open to false
|
||||
end
|
||||
end
|
||||
end">
|
||||
data-toggle-group="calendar-browser">
|
||||
<summary class="p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3">
|
||||
{% if calendar.post.feature_image %}
|
||||
<img src="{{ calendar.post.feature_image }}"
|
||||
@@ -35,8 +28,8 @@
|
||||
</div>
|
||||
</summary>
|
||||
<div class="p-4 border-t"
|
||||
hx-trigger="intersect once"
|
||||
hx-swap="innerHTML">
|
||||
sx-trigger="intersect once"
|
||||
sx-swap="innerHTML">
|
||||
<div class="text-sm text-stone-400">Loading calendar...</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
id="slot-description-title"
|
||||
{% if oob %}
|
||||
hx-swap-oob="outerHTML"
|
||||
sx-swap-oob="outerHTML"
|
||||
{% endif %}
|
||||
class="text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<div id="slot-errors" class="mt-2 text-sm text-red-600"></div>
|
||||
<form
|
||||
class="space-y-3 mt-4"
|
||||
hx-put="{{ url_for('calendar.slots.slot.put',
|
||||
sx-put="{{ url_for('calendar.slots.slot.put',
|
||||
calendar_slug=calendar.slug,
|
||||
slot_id=slot.id) }}"
|
||||
hx-target="#slot-{{ slot.id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if (event.detail.successful) this.reset()"
|
||||
sx-target="#slot-{{ slot.id }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-on:afterRequest="if (event.detail.successful) this.reset()"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -153,11 +153,11 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.cancel_button}}"
|
||||
hx-get="{{ url_for('calendar.slots.slot.get_view',
|
||||
sx-get="{{ url_for('calendar.slots.slot.get_view',
|
||||
calendar_slug=calendar.slug,
|
||||
slot_id=slot.id) }}"
|
||||
hx-target="#slot-{{ slot.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#slot-{{ slot.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -53,13 +53,13 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.pre_action_button}}"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.slots.slot.get_edit',
|
||||
slot_id=slot.id,
|
||||
calendar_slug=calendar.slug,
|
||||
) }}"
|
||||
hx-target="#slot-{{slot.id}}"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#slot-{{slot.id}}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<form
|
||||
hx-post="{{ url_for('calendar.slots.post',
|
||||
sx-post="{{ url_for('calendar.slots.post',
|
||||
calendar_slug=calendar.slug) }}"
|
||||
hx-target="#slots-table"
|
||||
hx-select="#slots-table"
|
||||
hx-disinherit="hx-select"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-target="#slots-table"
|
||||
sx-select="#slots-table"
|
||||
sx-disinherit="sx-select"
|
||||
sx-swap="outerHTML"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
class="space-y-3"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
@@ -98,10 +98,10 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.cancel_button}}"
|
||||
hx-get="{{ url_for('calendar.slots.add_button',
|
||||
sx-get="{{ url_for('calendar.slots.add_button',
|
||||
calendar_slug=calendar.slug) }}"
|
||||
hx-target="#slot-add-container"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#slot-add-container"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.pre_action_button}}"
|
||||
hx-get="{{ url_for('calendar.slots.add_form',
|
||||
sx-get="{{ url_for('calendar.slots.add_form',
|
||||
calendar_slug=calendar.slug) }}"
|
||||
hx-target="#slot-add-container"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#slot-add-container"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
+ Add slot
|
||||
</button>
|
||||
|
||||
@@ -45,14 +45,14 @@
|
||||
data-confirm-confirm-text="Yes, delete it"
|
||||
data-confirm-cancel-text="Cancel"
|
||||
data-confirm-event="confirmed"
|
||||
hx-delete="{{ url_for('calendar.slots.slot.slot_delete',
|
||||
sx-delete="{{ url_for('calendar.slots.slot.slot_delete',
|
||||
calendar_slug=calendar.slug,
|
||||
slot_id=s.id) }}"
|
||||
hx-target="#slots-table"
|
||||
hx-select="#slots-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
hx-trigger="confirmed"
|
||||
sx-target="#slots-table"
|
||||
sx-select="#slots-table"
|
||||
sx-swap="outerHTML"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-trigger="confirmed"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
|
||||
@@ -43,9 +43,9 @@
|
||||
<td class="px-4 py-2">
|
||||
{% if ticket.state in ('confirmed', 'reserved') %}
|
||||
<form
|
||||
hx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
|
||||
hx-target="#entry-ticket-row-{{ ticket.code }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
|
||||
sx-target="#entry-ticket-row-{{ ticket.code }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button
|
||||
|
||||
@@ -52,9 +52,9 @@
|
||||
<div id="checkin-action-{{ ticket.code }}">
|
||||
{% if ticket.state in ('confirmed', 'reserved') %}
|
||||
<form
|
||||
hx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
|
||||
hx-target="#checkin-action-{{ ticket.code }}"
|
||||
hx-swap="innerHTML"
|
||||
sx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
|
||||
sx-target="#checkin-action-{{ ticket.code }}"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
name="code"
|
||||
placeholder="Enter or scan ticket code..."
|
||||
class="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
hx-get="{{ url_for('ticket_admin.lookup') }}"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#lookup-result"
|
||||
hx-include="this"
|
||||
sx-get="{{ url_for('ticket_admin.lookup') }}"
|
||||
sx-trigger="keyup changed delay:300ms"
|
||||
sx-target="#lookup-result"
|
||||
sx-include="this"
|
||||
autofocus
|
||||
/>
|
||||
<button
|
||||
@@ -112,9 +112,9 @@
|
||||
<td class="px-4 py-3">
|
||||
{% if ticket.state in ('confirmed', 'reserved') %}
|
||||
<form
|
||||
hx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
|
||||
hx-target="#ticket-row-{{ ticket.code }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
|
||||
sx-target="#ticket-row-{{ ticket.code }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<button
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
<div id="ticket-errors" class="mt-2 text-sm text-red-600"></div>
|
||||
<form
|
||||
class="space-y-3 mt-4"
|
||||
hx-put="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.put',
|
||||
sx-put="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.put',
|
||||
calendar_slug=calendar.slug,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
entry_id=entry.id,
|
||||
ticket_type_id=ticket_type.id) }}"
|
||||
hx-target="#ticket-{{ ticket_type.id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-on::after-request="if (event.detail.successful) this.reset()"
|
||||
sx-target="#ticket-{{ ticket_type.id }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-on:afterRequest="if (event.detail.successful) this.reset()"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
@@ -70,15 +70,15 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.cancel_button}}"
|
||||
hx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_view',
|
||||
sx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_view',
|
||||
calendar_slug=calendar.slug,
|
||||
entry_id=entry.id,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
ticket_type_id=ticket_type.id) }}"
|
||||
hx-target="#ticket-{{ ticket_type.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#ticket-{{ ticket_type.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.pre_action_button}}"
|
||||
hx-get="{{ url_for(
|
||||
sx-get="{{ url_for(
|
||||
'calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit',
|
||||
ticket_type_id=ticket_type.id,
|
||||
calendar_slug=calendar.slug,
|
||||
@@ -41,8 +41,8 @@
|
||||
day=day,
|
||||
entry_id=entry.id,
|
||||
) }}"
|
||||
hx-target="#ticket-{{ticket_type.id}}"
|
||||
hx-swap="outerHTML"
|
||||
sx-target="#ticket-{{ticket_type.id}}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<form
|
||||
hx-post="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.post',
|
||||
sx-post="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.post',
|
||||
calendar_slug=calendar.slug,
|
||||
entry_id=entry.id,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
) }}"
|
||||
hx-target="#tickets-table"
|
||||
hx-select="#tickets-table"
|
||||
hx-disinherit="hx-select"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-target="#tickets-table"
|
||||
sx-select="#tickets-table"
|
||||
sx-disinherit="sx-select"
|
||||
sx-swap="outerHTML"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
class="space-y-3"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
@@ -55,15 +55,15 @@
|
||||
<button
|
||||
type="button"
|
||||
class="{{styles.cancel_button}}"
|
||||
hx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_button',
|
||||
sx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_button',
|
||||
calendar_slug=calendar.slug,
|
||||
entry_id=entry.id,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
) }}"
|
||||
hx-target="#ticket-add-container"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#ticket-add-container"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<button
|
||||
class="{{styles.action_button}}"
|
||||
hx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_form',
|
||||
sx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_form',
|
||||
calendar_slug=calendar.slug,
|
||||
entry_id=entry.id,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
) }}"
|
||||
hx-target="#ticket-add-container"
|
||||
hx-swap="innerHTML"
|
||||
sx-target="#ticket-add-container"
|
||||
sx-swap="innerHTML"
|
||||
>
|
||||
<i class="fa fa-plus"></i>
|
||||
Add ticket type
|
||||
|
||||
@@ -35,18 +35,18 @@
|
||||
data-confirm-confirm-text="Yes, delete it"
|
||||
data-confirm-cancel-text="Cancel"
|
||||
data-confirm-event="confirmed"
|
||||
hx-delete="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete',
|
||||
sx-delete="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete',
|
||||
calendar_slug=calendar.slug,
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
entry_id=entry.id,
|
||||
ticket_type_id=tt.id) }}"
|
||||
hx-target="#tickets-table"
|
||||
hx-select="#tickets-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
hx-trigger="confirmed"
|
||||
sx-target="#tickets-table"
|
||||
sx-select="#tickets-table"
|
||||
sx-swap="outerHTML"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-trigger="confirmed"
|
||||
type="button"
|
||||
>
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
{% if type_count == 0 %}
|
||||
{# Add to basket button #}
|
||||
<form
|
||||
hx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
hx-target="#ticket-buy-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
sx-target="#ticket-buy-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
class="flex items-center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
@@ -59,9 +59,9 @@
|
||||
{# +/- controls #}
|
||||
<div class="flex items-center gap-2">
|
||||
<form
|
||||
hx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
hx-target="#ticket-buy-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
sx-target="#ticket-buy-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="entry_id" value="{{ entry.id }}" />
|
||||
@@ -90,9 +90,9 @@
|
||||
</a>
|
||||
|
||||
<form
|
||||
hx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
hx-target="#ticket-buy-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
sx-target="#ticket-buy-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="entry_id" value="{{ entry.id }}" />
|
||||
@@ -128,9 +128,9 @@
|
||||
{% if qty == 0 %}
|
||||
{# Add to basket button #}
|
||||
<form
|
||||
hx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
hx-target="#ticket-buy-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
sx-target="#ticket-buy-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
class="flex items-center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
@@ -149,9 +149,9 @@
|
||||
{# +/- controls #}
|
||||
<div class="flex items-center gap-2">
|
||||
<form
|
||||
hx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
hx-target="#ticket-buy-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
sx-target="#ticket-buy-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="entry_id" value="{{ entry.id }}" />
|
||||
@@ -179,9 +179,9 @@
|
||||
</a>
|
||||
|
||||
<form
|
||||
hx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
hx-target="#ticket-buy-{{ entry.id }}"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('tickets.adjust_quantity') }}"
|
||||
sx-target="#ticket-buy-{{ entry.id }}"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
|
||||
<input type="hidden" name="entry_id" value="{{ entry.id }}" />
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{# Account nav items: tickets + bookings links for the account dashboard #}
|
||||
<div class="relative nav-group">
|
||||
<a href="{{ account_url('/tickets/') }}"
|
||||
hx-get="{{ account_url('/tickets/') }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ account_url('/tickets/') }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="{{styles.nav_button}}">
|
||||
tickets
|
||||
</a>
|
||||
</div>
|
||||
<div class="relative nav-group">
|
||||
<a href="{{ account_url('/bookings/') }}"
|
||||
hx-get="{{ account_url('/bookings/') }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ account_url('/bookings/') }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="{{styles.nav_button}}">
|
||||
bookings
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user