Delete 391 dead Jinja templates replaced by sx_components/defpage
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m13s

All app-level templates have been replaced by native sx component builders
and defpage declarative routes. Removes ~15,200 lines of dead HTML.

Kept: shared/browser templates (errors, ap_social, macros, root layout),
account + federation _email/magic_link, federation profile.html chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 15:10:56 +00:00
parent c243d17eeb
commit 544892edd9
391 changed files with 0 additions and 15208 deletions

View File

@@ -1,62 +0,0 @@
{# List card for all events — one entry #}
{% set pi = page_info.get(entry.calendar_container_id, {}) %}
{% set page_slug = pi.get('slug', '') %}
{% set page_title = pi.get('title') %}
<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">
{# Left: event info #}
<div class="flex-1 min-w-0">
{% if page_slug %}
{% set day_href = events_url('/' ~ page_slug ~ '/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %}
{% else %}
{% set day_href = '' %}
{% endif %}
{% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' if day_href else '' %}
{% if entry_href %}
<a href="{{ entry_href }}" class="hover:text-emerald-700">
<h2 class="text-lg font-semibold text-stone-900">{{ entry.name }}</h2>
</a>
{% else %}
<h2 class="text-lg font-semibold text-stone-900">{{ entry.name }}</h2>
{% endif %}
<div class="flex flex-wrap items-center gap-1.5 mt-1">
{% if page_title %}
<a href="{{ events_url('/' ~ page_slug ~ '/') }}"
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200">
{{ page_title }}
</a>
{% endif %}
{% if entry.calendar_name %}
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700">
{{ entry.calendar_name }}
</span>
{% endif %}
</div>
<div class="mt-1 text-sm text-stone-500">
{% if day_href %}
<a href="{{ day_href }}" class="hover:text-stone-700">{{ entry.start_at.strftime('%a %-d %b') }}</a> &middot;
{% else %}
{{ entry.start_at.strftime('%a %-d %b') }} &middot;
{% endif %}
{{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} &ndash; {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
{% if entry.cost %}
<div class="mt-1 text-sm font-medium text-green-600">
&pound;{{ '%.2f'|format(entry.cost) }}
</div>
{% endif %}
</div>
{# Right: ticket widget #}
{% if entry.ticket_price is not none %}
<div class="shrink-0">
{% set qty = pending_tickets.get(entry.id, 0) %}
{% set ticket_url = url_for('all_events.adjust_ticket') %}
{% include '_types/page_summary/_ticket_widget.html' %}
</div>
{% endif %}
</div>
</article>

View File

@@ -1,60 +0,0 @@
{# Tile card for all events — compact event tile #}
{% set pi = page_info.get(entry.calendar_container_id, {}) %}
{% set page_slug = pi.get('slug', '') %}
{% set page_title = pi.get('title') %}
<article class="rounded-xl bg-white shadow-sm border border-stone-200 overflow-hidden">
{% if page_slug %}
{% set day_href = events_url('/' ~ page_slug ~ '/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %}
{% else %}
{% set day_href = '' %}
{% endif %}
{% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' if day_href else '' %}
<div class="p-3">
{% if entry_href %}
<a href="{{ entry_href }}" class="hover:text-emerald-700">
<h2 class="text-base font-semibold text-stone-900 line-clamp-2">{{ entry.name }}</h2>
</a>
{% else %}
<h2 class="text-base font-semibold text-stone-900 line-clamp-2">{{ entry.name }}</h2>
{% endif %}
<div class="flex flex-wrap items-center gap-1 mt-1">
{% if page_title %}
<a href="{{ events_url('/' ~ page_slug ~ '/') }}"
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200">
{{ page_title }}
</a>
{% endif %}
{% if entry.calendar_name %}
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700">
{{ entry.calendar_name }}
</span>
{% endif %}
</div>
<div class="mt-1 text-xs text-stone-500">
{% if day_href %}
<a href="{{ day_href }}" class="hover:text-stone-700">{{ entry.start_at.strftime('%a %-d %b') }}</a>
{% else %}
{{ entry.start_at.strftime('%a %-d %b') }}
{% endif %}
&middot;
{{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} &ndash; {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
{% if entry.cost %}
<div class="mt-1 text-sm font-medium text-green-600">
&pound;{{ '%.2f'|format(entry.cost) }}
</div>
{% endif %}
</div>
{# Ticket widget below card #}
{% if entry.ticket_price is not none %}
<div class="border-t border-stone-100 px-3 py-2">
{% set qty = pending_tickets.get(entry.id, 0) %}
{% set ticket_url = url_for('all_events.adjust_ticket') %}
{% include '_types/page_summary/_ticket_widget.html' %}
</div>
{% endif %}
</article>

View File

@@ -1,31 +0,0 @@
{% for entry in entries %}
{% if view == 'tile' %}
{% include "_types/all_events/_card_tile.html" %}
{% else %}
{# Date header when date changes (list view only) #}
{% set entry_date = entry.start_at.strftime('%A %-d %B %Y') %}
{% if loop.first or entry_date != entries[loop.index0 - 1].start_at.strftime('%A %-d %B %Y') %}
<div class="pt-2 pb-1">
<h3 class="text-sm font-semibold text-stone-500 uppercase tracking-wide">
{{ entry_date }}
</h3>
</div>
{% endif %}
{% include "_types/all_events/_card.html" %}
{% endif %}
{% endfor %}
{% if has_more %}
{# Infinite scroll sentinel #}
{% set entries_url = url_for('all_events.entries_fragment', page=page + 1, view=view if view != 'list' else '')|host %}
<div
id="sentinel-{{ page }}"
class="h-4 opacity-0 pointer-events-none"
sx-get="{{ entries_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...</div>
</div>
{% endif %}

View File

@@ -1,54 +0,0 @@
{# View toggle bar - desktop only #}
<div class="hidden md:flex justify-end px-3 pt-3 gap-1">
{% set list_href = (current_local_href ~ {'view': None}|qs)|host %}
{% set tile_href = (current_local_href ~ {'view': 'tile'}|qs)|host %}
<a
href="{{ list_href }}"
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"
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" />
</svg>
</a>
<a
href="{{ tile_href }}"
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"
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" />
</svg>
</a>
</div>
{# Cards container - list or grid based on view #}
{% if entries %}
{% if view == 'tile' %}
<div class="max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{% include "_types/all_events/_cards.html" %}
</div>
{% else %}
<div class="max-w-full px-3 py-3 space-y-3">
{% include "_types/all_events/_cards.html" %}
</div>
{% endif %}
{% else %}
<div class="px-3 py-12 text-center text-stone-400">
<i class="fa fa-calendar-xmark text-4xl mb-3" aria-hidden="true"></i>
<p class="text-lg">No upcoming events</p>
</div>
{% endif %}
<div class="pb-8"></div>

View File

@@ -1,7 +0,0 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block content %}
{% include '_types/all_events/_main_panel.html' %}
{% endblock %}

View File

@@ -1,12 +0,0 @@
{% macro description(calendar, oob=False) %}
<div
id="calendar-description-title"
{% if oob %}
sx-swap-oob="outerHTML"
{% endif %}
class="text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
>
{{ calendar.description or ''}}
</div>
{% endmacro %}

View File

@@ -1,170 +0,0 @@
<section class="bg-orange-100">
<header class="flex items-center justify-center mt-2">
{# Month / year navigation #}
<nav class="flex items-center gap-2 text-2xl">
{# Outer left: -1 year #}
<a
class="{{styles.pill}} text-xl"
href="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=prev_year,
month=month) }}"
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=prev_year,
month=month) }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
>
&laquo;
</a>
{# Inner left: -1 month #}
<a
class="{{styles.pill}} text-xl"
href="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=prev_month_year,
month=prev_month) }}"
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=prev_month_year,
month=prev_month) }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
>
&lsaquo;
</a>
<div class="px-3 font-medium">
{{ month_name }} {{ year }}
</div>
{# Inner right: +1 month #}
<a
class="{{styles.pill}} text-xl"
href="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=next_month_year,
month=next_month) }}"
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=next_month_year,
month=next_month) }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
>
&rsaquo;
</a>
{# Outer right: +1 year #}
<a
class="{{styles.pill}} text-xl"
href="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=next_year,
month=month) }}"
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=next_year,
month=month) }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
>
&raquo;
</a>
</nav>
</header>
{# Calendar grid #}
<div class="rounded-2xl border border-stone-200 bg-white/80 p-4">
{# Weekday header: only show on sm+ (desktop/tablet) #}
<div class="hidden sm:grid grid-cols-7 text-center text-md font-semibold text-stone-700 mb-2">
{% for wd in weekday_names %}
<div class="py-1">{{ wd }}</div>
{% endfor %}
</div>
{# On mobile: 1 column; on sm+: 7 columns #}
<div class="grid grid-cols-1 sm:grid-cols-7 gap-px bg-stone-200 rounded-xl overflow-hidden">
{% for week in weeks %}
{% for day in week %}
<div
class="min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs {% if not day.in_month %} bg-stone-50 text-stone-400{% endif %} {% if day.is_today %} ring-2 ring-blue-500 z-10 relative {% endif %}"
>
<div class="flex justify-between items-center">
<div class="flex flex-col">
<span class="sm:hidden text-[16px] text-stone-500">
{{ day.date.strftime('%a') }}
</span>
{# Clickable day number: goes to day detail view #}
<a
class="{{styles.pill}}"
href="{{ url_for('calendar.day.show_day',
calendar_slug=calendar.slug,
year=day.date.year,
month=day.date.month,
day=day.date.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) }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
>
{{ day.date.day }}
</a>
</div>
</div>
{# Entries for this day: merged, chronological #}
<div class="mt-1 space-y-0.5">
{# Build a list of entries for this specific day.
month_entries is already sorted by start_at in Python. #}
{% for e in month_entries %}
{% if e.start_at.date() == day.date %}
{# Decide colour: highlight "mine" differently if you want #}
{% set is_mine = (g.user and e.user_id == g.user.id)
or (not g.user and e.session_id == qsession.get('calendar_sid')) %}
<div class="flex items-center justify-between gap-1 text-[11px] rounded px-1 py-0.5
{% if e.state == 'confirmed' %}
{% if is_mine %}
bg-emerald-200 text-emerald-900
{% else %}
bg-emerald-100 text-emerald-800
{% endif %}
{% else %}
{% if is_mine %}
bg-sky-100 text-sky-800
{% else %}
bg-stone-100 text-stone-700
{% endif %}
{% endif %}">
<span class="truncate">
{{ e.name }}
</span>
<span class="shrink-0 text-[10px] font-semibold uppercase tracking-tight">
{{ (e.state or 'pending')|replace('_', ' ') }}
</span>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>

View File

@@ -1,18 +0,0 @@
<!-- Desktop nav -->
{% import 'macros/links.html' as links %}
{% call links.link(
url_for('calendar.slots.defpage_slots_listing', calendar_slug=calendar.slug),
hx_select_search,
select_colours,
True,
aclass=styles.nav_button
) %}
<i class="fa fa-clock" aria-hidden="true"></i>
<div>
Slots
</div>
{% endcall %}
{% if g.rights.admin %}
{% from 'macros/admin_nav.html' import admin_nav_item %}
{{ admin_nav_item(url_for('calendar.admin.defpage_calendar_admin', calendar_slug=calendar.slug)) }}
{% endif %}

View File

@@ -1,22 +0,0 @@
{% extends "oob_elements.html" %}
{# OOB elements for post admin page #}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('post-header-child', 'calendar-header-child', '_types/calendar/header/_header.html')}}
{% from '_types/post/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/calendar/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/calendar/_main_panel.html' %}
{% endblock %}

View File

@@ -1,32 +0,0 @@
<div id="calendar-description">
{% if calendar.description %}
<p class="text-stone-700 whitespace-pre-line break-all">
{{ calendar.description }}
</p>
{% else %}
<p class="text-stone-400 italic">
No description yet.
</p>
{% endif %}
<button
type="button"
class="mt-2 text-xs underline"
sx-get="{{ url_for(
'calendar.admin.calendar_description_edit',
calendar_slug=calendar.slug,
) }}"
sx-target="#calendar-description"
sx-swap="outerHTML"
>
<i class="fas fa-edit"></i>
</button>
</div>
{% if oob %}
{% from '_types/calendar/_description.html' import description %}
{{description(calendar, oob=True)}}
{% endif %}

View File

@@ -1,41 +0,0 @@
<div id="calendar-description">
<form
sx-post="{{ url_for(
'calendar.admin.calendar_description_save',
calendar_slug=calendar.slug,
) }}"
sx-target="#calendar-description"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<textarea
name="description"
autocomplete="off"
rows="4"
class="w-full p-2 border rounded"
>{{ calendar.description or '' }}</textarea>
<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>
<button
type="button"
class="px-3 py-1 rounded border"
sx-get="{{ url_for(
'calendar.admin.calendar_description_view',
calendar_slug=calendar.slug,
) }}"
sx-target="#calendar-description"
sx-swap="outerHTML"
>
Cancel
</button>
</div>
</form>
</div>

View File

@@ -1,45 +0,0 @@
<section class="max-w-3xl mx-auto p-4 space-y-10">
<!-- Calendar config -->
<div>
<h2 class="text-xl font-semibold">Calendar configuration</h2>
<div id="cal-put-errors" class="mt-2 text-sm text-red-600"></div>
<div>
<label class="block text-sm font-medium text-stone-700">
Description
</label>
{% include '_types/calendar/admin/_description.html' %}
</div>
<form
id="calendar-form"
method="post"
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"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div>
<label class="block text-sm font-medium text-stone-700">Description</label>
<div>{{calendar.description or ''}}</div>
<textarea
name="description"
autocomplete="off"
rows="4" class="w-full p-2 border rounded"
>{{ (calendar.description or '') }}</textarea>
</div>
<div>
<button class="px-3 py-2 rounded bg-stone-800 text-white">Save</button>
</div>
</form>
</div>
<hr class="border-stone-200">
</section>

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,25 +0,0 @@
{% extends 'oob_elements.html' %}
{# OOB elements for calendar admin page #}
{# Import shared OOB macros #}
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('calendar-header-child', 'calendar-admin-header-child', '_types/calendar/admin/header/_header.html')}}
{% from '_types/calendar/header/_header.html' import header_row with context %}
{{header_row(oob=True)}}
{% endblock %}
{% block mobile_menu %}
{% include '_types/calendar/admin/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/calendar/admin/_main_panel.html' %}
{% endblock %}

View File

@@ -1,13 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='calendar-admin-row', oob=oob) %}
{% call links.link(
hx_select_search
) %}
{{ links.admin() }}
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/calendar/admin/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,24 +0,0 @@
{% extends '_types/calendar/index.html' %}
{% import 'macros/layout.html' as layout %}
{% block calendar_header_child %}
{% from '_types/root/_n/macros.html' import header with context %}
{% call header() %}
{% from '_types/calendar/admin/header/_header.html' import header_row with context %}
{{ header_row() }}
<div id="calendar-admin-header-child">
{% block calendar_admin_header_child %}
{% endblock %}
</div>
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/calendar/admin/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/calendar/admin/_main_panel.html' %}
{% endblock %}

View File

@@ -1,23 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='calendar-row', oob=oob) %}
{% call links.link(url_for('calendar.get', calendar_slug=calendar.slug), hx_select_search) %}
<div class="flex flex-col md:flex-row md:gap-2 items-center min-w-0">
<div class="flex flex-row items-center gap-2">
<i class="fa fa-calendar"></i>
<div class="shrink-0">
{{ calendar.name }}
</div>
</div>
{% from '_types/calendar/_description.html' import description %}
{{description(calendar)}}
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/calendar/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,26 +0,0 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block root_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
{% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
{% call index_row('calendar-header-child', '_types/calendar/header/_header.html') %}
{% block calendar_header_child %}
{% endblock %}
{% endcall %}
{% endcall %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/calendar/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/calendar/_main_panel.html' %}
{% endblock %}

View File

@@ -1,44 +0,0 @@
{% for row in calendars %}
{% set cal = row %}
<div class="mt-6 border rounded-lg p-4">
<div class="flex items-center justify-between gap-3">
{% set calendar_href = url_for('calendar.get', calendar_slug=cal.slug)|host %}
<a
class="flex items-baseline gap-3"
href="{{ calendar_href }}"
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>
</a>
<!-- Soft delete -->
<button
class="text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
data-confirm
data-confirm-title="Delete calendar?"
data-confirm-text="Entries will be hidden (soft delete)"
data-confirm-icon="warning"
data-confirm-confirm-text="Yes, delete it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
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>
</div>
</div>
{% else %}
<p class="text-gray-500 mt-4">No calendars yet. Create one above.</p>
{% endfor %}

View File

@@ -1,27 +0,0 @@
<section class="p-4">
{% if has_access('calendars.create_calendar') %}
<!-- error container under the inputs -->
<div id="cal-create-errors" class="mt-2 text-sm text-red-600"></div>
<form
class="mt-4 flex gap-2 items-end"
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">
<label class="block text-sm text-gray-600">Name</label>
<input name="name" type="text" required class="w-full border rounded px-3 py-2" placeholder="e.g. Events, Gigs, Meetings" />
</div>
<button type="submit" class="border rounded px-3 py-2">Add calendar</button>
</form>
{% endif %}
<!-- list -->
<div id="calendars-list" class="mt-6">
{% include "_types/calendars/_calendars_list.html" %}
</div>
</section>

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,28 +0,0 @@
{% extends 'oob_elements.html' %}
{# OOB elements for HTMX navigation - all elements that need updating #}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
{# Header with app title - includes cart-mini, navigation, and market-specific header #}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('post-admin-header-child', 'calendars-header-child', '_types/calendars/header/_header.html')}}
{% from '_types/post/admin/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/calendars/_nav.html' %}
{% endblock %}
{% block content %}
{% include "_types/calendars/_main_panel.html" %}
{% endblock %}

View File

@@ -1,14 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='calendars-row', oob=oob) %}
{% call links.link(url_for('calendars.home'), hx_select_search) %}
<i class="fa fa-calendar" aria-hidden="true"></i>
<div>
Calendars
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/calendars/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,26 +0,0 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block root_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
{% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
{% call index_row('calendars-header-child', '_types/calendars/header/_header.html') %}
{% block calendars_header_child %}
{% endblock %}
{% endcall %}
{% endcall %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/calendars/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/calendars/_main_panel.html' %}
{% endblock %}

View File

@@ -1,299 +0,0 @@
<div id="entry-errors" class="mt-2 text-sm text-red-600"></div>
<form
class="mt-4 grid grid-cols-1 md:grid-cols-4 gap-2"
sx-post="{{ url_for(
'calendar.day.calendar_entries.add_entry',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
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() }}">
{# 1) Entry name #}
<input
name="name"
type="text"
required
class="border rounded px-3 py-2"
placeholder="Entry name"
/>
{# 2) Slot picker for this weekday (required) #}
{% if day_slots %}
<select
name="slot_id"
class="border rounded px-3 py-2"
data-slot-picker
required
>
{% for slot in day_slots %}
<option
value="{{ slot.id }}"
data-start="{{ slot.time_start.strftime('%H:%M') }}"
data-end="{{ slot.time_end.strftime('%H:%M') if slot.time_end else '' }}"
data-flexible="{{ '1' if slot | getattr('flexible', False) else '0' }}"
data-cost="{{ slot.cost if slot.cost is not none else '0' }}"
>
{{ slot.name }}
({{ slot.time_start.strftime('%H:%M') }}
{% if slot.time_end %}{{ slot.time_end.strftime('%H:%M') }}{% else %}open-ended{% endif %})
{% if slot | getattr('flexible', False) %}[flexible]{% endif %}
</option>
{% endfor %}
</select>
{% else %}
<div class="text-sm text-stone-500">
No slots defined for this day.
</div>
{% endif %}
{# 3) Time entry + cost display #}
<div class="md:col-span-2 flex flex-col gap-2">
{# Time inputs — hidden until a flexible slot is selected #}
<div data-time-fields class="hidden">
<div class="mb-2">
<label class="block text-xs font-medium text-stone-700 mb-1">From</label>
<input
name="start_time"
type="time"
class="border rounded px-3 py-2 w-full"
data-entry-start
/>
</div>
<div class="mb-2">
<label class="block text-xs font-medium text-stone-700 mb-1">To</label>
<input
name="end_time"
type="time"
class="border rounded px-3 py-2 w-full"
data-entry-end
/>
</div>
<p class="text-xs text-stone-500" data-slot-boundary></p>
</div>
{# Cost display — shown when a slot is selected #}
<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</span>
</div>
{# Summary of fixed times — shown for non-flexible slots #}
<div data-fixed-summary class="hidden text-sm text-stone-600"></div>
</div>
{# 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)</h4>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-stone-700 mb-1">
Ticket Price (£)
</label>
<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>
<div>
<label class="block text-xs font-medium text-stone-700 mb-1">
Total Tickets
</label>
<input
name="ticket_count"
type="number"
min="0"
class="w-full border rounded px-3 py-2 text-sm"
placeholder="Leave empty for unlimited"
/>
</div>
</div>
</div>
<div class="flex justify-end gap-2 pt-2 md:col-span-4">
<button
type="button"
class="{{styles.cancel_button}}"
sx-get="{{ url_for('calendar.day.calendar_entries.add_button',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
sx-target="#entry-add-container"
sx-swap="innerHTML"
>
Cancel
</button>
<button
type="submit"
class="{{styles.action_button}}"
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"></i>
Save entry
</button>
</div>
</form>
{# --- Behaviour: lock / unlock times based on slot.flexible --- #}
<script>
(function () {
function timeToMinutes(timeStr) {
if (!timeStr) return 0;
const [hours, minutes] = timeStr.split(':').map(Number);
return hours * 60 + minutes;
}
function calculateCost(slotCost, slotStart, slotEnd, actualStart, actualEnd, flexible) {
if (!flexible) {
// Fixed slot: use full slot cost
return parseFloat(slotCost);
}
// Flexible slot: prorate based on time range
if (!actualStart || !actualEnd) return 0;
const slotStartMin = timeToMinutes(slotStart);
const slotEndMin = timeToMinutes(slotEnd);
const actualStartMin = timeToMinutes(actualStart);
const actualEndMin = timeToMinutes(actualEnd);
const slotDuration = slotEndMin - slotStartMin;
const actualDuration = actualEndMin - actualStartMin;
if (slotDuration <= 0 || actualDuration <= 0) return 0;
const ratio = actualDuration / slotDuration;
return parseFloat(slotCost) * ratio;
}
function initEntrySlotPicker(root, applyInitial = false) {
const select = root.querySelector('[data-slot-picker]');
if (!select) return;
const timeFields = root.querySelector('[data-time-fields]');
const startInput = root.querySelector('[data-entry-start]');
const endInput = root.querySelector('[data-entry-end]');
const helper = root.querySelector('[data-slot-boundary]');
const costDisplay = root.querySelector('[data-cost-display]');
const costRow = root.querySelector('[data-cost-row]');
const fixedSummary = root.querySelector('[data-fixed-summary]');
if (!startInput || !endInput) return;
function updateCost() {
const opt = select.selectedOptions[0];
if (!opt || !opt.value) {
if (costDisplay) costDisplay.textContent = '£0.00';
return;
}
const cost = opt.dataset.cost || '0';
const s = opt.dataset.start || '';
const e = opt.dataset.end || '';
const flexible = opt.dataset.flexible === '1';
const calculatedCost = calculateCost(
cost, s, e,
startInput.value, endInput.value,
flexible
);
if (costDisplay) {
costDisplay.textContent = '£' + calculatedCost.toFixed(2);
}
}
function applyFromOption(opt) {
if (!opt || !opt.value) {
if (timeFields) timeFields.classList.add('hidden');
if (costRow) costRow.classList.add('hidden');
if (fixedSummary) fixedSummary.classList.add('hidden');
return;
}
const s = opt.dataset.start || '';
const e = opt.dataset.end || '';
const flexible = opt.dataset.flexible === '1';
if (!flexible) {
// Fixed slot: hide time inputs, show summary + cost
if (s) startInput.value = s;
if (e) endInput.value = e;
if (timeFields) timeFields.classList.add('hidden');
if (fixedSummary) {
fixedSummary.classList.remove('hidden');
if (e) {
fixedSummary.textContent = `${s} ${e}`;
} else {
fixedSummary.textContent = `From ${s} (open-ended)`;
}
}
if (costRow) costRow.classList.remove('hidden');
} else {
// Flexible slot: show time inputs, hide fixed summary, show cost
if (timeFields) timeFields.classList.remove('hidden');
if (fixedSummary) fixedSummary.classList.add('hidden');
if (costRow) costRow.classList.remove('hidden');
if (helper) {
if (e) {
helper.textContent = `Times must be between ${s} and ${e}.`;
} else {
helper.textContent = `Start at or after ${s}.`;
}
}
}
updateCost();
}
// Only apply initial state if explicitly requested (on first load)
if (applyInitial) {
applyFromOption(select.selectedOptions[0]);
}
// Remove any existing listener to prevent duplicates
if (select._slotChangeHandler) {
select.removeEventListener('change', select._slotChangeHandler);
}
select._slotChangeHandler = () => {
applyFromOption(select.selectedOptions[0]);
};
select.addEventListener('change', select._slotChangeHandler);
// Update cost when times change (for flexible slots)
startInput.addEventListener('input', updateCost);
endInput.addEventListener('input', updateCost);
}
// Initial load - apply initial state
document.addEventListener('DOMContentLoaded', () => {
initEntrySlotPicker(document, true);
});
// HTMX fragments - apply initial state so visibility is correct
if (window.htmx) {
htmx.onLoad((content) => {
initEntrySlotPicker(content, true);
});
}
})();
</script>

View File

@@ -1,16 +0,0 @@
<button
type="button"
class="{{styles.pre_action_button}}"
sx-get="{{ url_for(
'calendar.day.calendar_entries.add_form',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
sx-target="#entry-add-container"
sx-swap="innerHTML"
>
+ Add entry
</button>

View File

@@ -1,28 +0,0 @@
<section id="day-entries" class="{{styles.list_container}}">
<table class="w-full text-sm border table-fixed">
<thead class="bg-stone-100">
<tr>
<th class="p-2 text-left w-2/6">Name</th>
<th class="text-left p-2 w-1/6">Slot/Time</th>
<th class="text-left p-2 w-1/6">State</th>
<th class="text-left p-2 w-1/6">Cost</th>
<th class="text-left p-2 w-1/6">Tickets</th>
<th class="text-left p-2 w-1/6">Actions</th>
</tr>
</thead>
<tbody>
{% for entry in day_entries %}
{% include '_types/day/_row.html' %}
{% else %}
<tr><td colspan="6" class="p-3 text-stone-500">No entries yet.</td></tr>
{% endfor %}
</tbody>
</table>
<div id="entry-add-container" class="mt-4">
{% include '_types/day/_add_button.html' %}
</div>
</section>

View File

@@ -1,39 +0,0 @@
{% import 'macros/links.html' as links %}
{# Confirmed Entries - vertical on mobile, horizontal with arrows on desktop #}
<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">
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
{% call(entry) scrolling_menu('day-entries-container', confirmed_entries) %}
<a
href="{{ url_for('calendar.day.calendar_entries.calendar_entry.get',
calendar_slug=calendar.slug,
year=day_date.year,
month=day_date.month,
day=day_date.day,
entry_id=entry.id) }}"
class="flex items-center gap-2 px-3 py-2 hover:bg-stone-100 rounded transition text-sm border sm:whitespace-nowrap sm:flex-shrink-0">
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{{ entry.name }}</div>
<div class="text-xs text-stone-600 truncate">
{{ entry.start_at.strftime('%H:%M') }}
{% if entry.end_at %} {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
</div>
</a>
{% endcall %}
</div>
{# Admin link #}
{% if g.rights.admin %}
{% from 'macros/admin_nav.html' import admin_nav_item %}
{{admin_nav_item(
url_for(
'calendar.day.admin.admin',
calendar_slug=calendar.slug,
year=day_date.year,
month=day_date.month,
day=day_date.day
)
)}}
{% endif %}

View File

@@ -1,18 +0,0 @@
{% extends "oob_elements.html" %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('calendar-header-child', 'day-header-child', '_types/day/header/_header.html')}}
{% from '_types/calendar/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/day/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/day/_main_panel.html' %}
{% endblock %}

View File

@@ -1,74 +0,0 @@
{% import 'macros/links.html' as links %}
<tr class="{{ styles.tr }}">
<td class="p-2 align-top w-2/6">
<div class="font-medium">
{% call links.link(
url_for(
'calendar.day.calendar_entries.calendar_entry.get',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
),
hx_select_search,
aclass=styles.pill
) %}
{{ entry.name }}
{% endcall %}
</div>
</td>
<td class="p-2 align-top w-1/6">
{% if entry.slot %}
<div class="text-xs font-medium">
{% call links.link(
url_for(
'calendar.slots.slot.defpage_slot_detail',
calendar_slug=calendar.slug,
slot_id=entry.slot.id
),
hx_select_search,
aclass=styles.pill
) %}
{{ entry.slot.name }}
{% endcall %}
<span class="text-stone-600 font-normal">
({{ entry.slot.time_start.strftime('%H:%M') }}{% if entry.slot.time_end %} → {{ entry.slot.time_end.strftime('%H:%M') }}{% endif %})
</span>
</div>
{% else %}
<div class="text-xs text-stone-600">
{% include '_types/entry/_times.html' %}
</div>
{% endif %}
</td>
<td class="p-2 align-top w-1/6">
<div id="entry-state-{{entry.id}}">
{% include '_types/entry/_state.html' %}
</div>
</td>
<td class="p-2 align-top w-1/6">
<span class="font-medium text-green-600">
£{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }}
</span>
</td>
<td class="p-2 align-top w-1/6">
{% if entry.ticket_price is not none %}
<div class="text-xs space-y-1">
<div class="font-medium text-green-600">£{{ ('%.2f'|format(entry.ticket_price)) }}</div>
<div class="text-stone-600">
{% if entry.ticket_count is not none %}
{{ entry.ticket_count }} tickets
{% else %}
Unlimited
{% endif %}
</div>
</div>
{% else %}
<span class="text-xs text-stone-400">No tickets</span>
{% endif %}
</td>
<td class="p-2 align-top w-1/6">
{% include '_types/entry/_options.html' %}
</td>
</tr>

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,33 +0,0 @@
{# OOB swap for day confirmed entries nav when entries are edited #}
{% import 'macros/links.html' as links %}
{# Confirmed Entries - vertical on mobile, horizontal with arrows on desktop #}
{% 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"
sx-swap-oob="true">
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
{% call(entry) scrolling_menu('day-entries-container', confirmed_entries) %}
<a
href="{{ url_for('calendar.day.calendar_entries.calendar_entry.get',
calendar_slug=calendar.slug,
year=day_date.year,
month=day_date.month,
day=day_date.day,
entry_id=entry.id) }}"
class="{{styles.nav_button}}"
>
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{{ entry.name }}</div>
<div class="text-xs text-stone-600 truncate">
{{ entry.start_at.strftime('%H:%M') }}
{% if entry.end_at %} {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
</div>
</a>
{% endcall %}
</div>
{% else %}
{# Empty placeholder to remove nav entries when none are confirmed #}
<div id="day-entries-nav-wrapper" sx-swap-oob="true"></div>
{% endif %}

View File

@@ -1,25 +0,0 @@
{% extends 'oob_elements.html' %}
{# OOB elements for calendar admin page #}
{# Import shared OOB macros #}
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('day-header-child', 'day-admin-header-child', '_types/day/admin/header/_header.html')}}
{% from '_types/calendar/header/_header.html' import header_row with context %}
{{header_row(oob=True)}}
{% endblock %}
{% block mobile_menu %}
{% include '_types/day/admin/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/day/admin/_main_panel.html' %}
{% endblock %}

View File

@@ -1,20 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='day-admin-row', oob=oob) %}
{% call links.link(
url_for(
'calendar.day.admin.admin',
calendar_slug=calendar.slug,
year=day_date.year,
month=day_date.month,
day=day_date.day
),
hx_select_search
) %}
{{ links.admin() }}
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/day/admin/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,24 +0,0 @@
{% extends '_types/day/index.html' %}
{% import 'macros/layout.html' as layout %}
{% import 'macros/links.html' as links %}
{% block day_header_child %}
{% from '_types/root/_n/macros.html' import header with context %}
{% call header() %}
{% from '_types/day/admin/header/_header.html' import header_row with context %}
{{ header_row() }}
<div id="day-admin-header-child">
{% block day_admin_header_child %}
{% endblock %}
</div>
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/day/admin/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/day/admin/_main_panel.html' %}
{% endblock %}

View File

@@ -1,26 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='day-row', oob=oob) %}
{% call links.link(
url_for(
'calendar.day.show_day',
calendar_slug=calendar.slug,
year=day_date.year,
month=day_date.month,
day=day_date.day
),
hx_select_search,
) %}
<div class="flex gap-1 items-center">
<i class="fa fa-calendar-day"></i>
{{ day_date.strftime('%A %d %B %Y') }}
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/day/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,18 +0,0 @@
{% extends '_types/calendar/index.html' %}
{% block calendar_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('day-header-child', '_types/day/header/_header.html') %}
{% block day_header_child %}
{% endblock %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/day/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/day/_main_panel.html' %}
{% endblock %}

View File

@@ -1,332 +0,0 @@
<section id="entry-{{ entry.id }}"
class="{{styles.list_container}}">
<!-- Error container -->
<div id="entry-errors-{{ entry.id }}" class="mt-2 text-sm text-red-600"></div>
<form
class="space-y-3 mt-4"
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
) }}"
sx-target="#entry-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- Name -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1"
for="entry-name-{{ entry.id }}">
Name
</label>
<input
id="entry-name-{{ entry.id }}"
name="name"
class="w-full border p-2 rounded"
placeholder="Name"
value="{{ entry.name }}"
>
</div>
<!-- Slot picker -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1"
for="entry-slot-{{ entry.id }}">
Slot
</label>
{% if day_slots %}
<select
id="entry-slot-{{ entry.id }}"
name="slot_id"
class="w-full border p-2 rounded"
data-slot-picker
required
>
{% for slot in day_slots %}
<option
value="{{ slot.id }}"
data-start="{{ slot.time_start.strftime('%H:%M') }}"
data-end="{{ slot.time_end.strftime('%H:%M') if slot.time_end else '' }}"
data-flexible="{{ '1' if slot.flexible else '0' }}"
data-cost="{{ slot.cost if slot.cost is not none else '0' }}"
{% if entry.slot_id == slot.id %}selected{% endif %}
>
{{ slot.name }}
({{ slot.time_start.strftime('%H:%M') }}
{% if slot.time_end %}{{ slot.time_end.strftime('%H:%M') }}{% else %}open-ended{% endif %})
{% if slot.flexible %}[flexible]{% endif %}
</option>
{% endfor %}
</select>
{% else %}
<div class="text-sm text-stone-500">
No slots defined for this day.
</div>
{% endif %}
</div>
<!-- Time inputs — shown only for 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="entry-start-{{ entry.id }}">
From
</label>
<input
id="entry-start-{{ entry.id }}"
name="start_at"
type="time"
class="w-full border p-2 rounded"
value="{{ entry.start_at.strftime('%H:%M') if entry.start_at else '' }}"
data-entry-start
>
</div>
<div>
<label class="block text-sm font-medium text-stone-700 mb-1"
for="entry-end-{{ entry.id }}">
To
</label>
<input
id="entry-end-{{ entry.id }}"
name="end_at"
type="time"
class="w-full border p-2 rounded"
value="{{ entry.end_at.strftime('%H:%M') if entry.end_at else '' }}"
data-entry-end
>
</div>
<p class="text-xs text-stone-500" data-slot-boundary></p>
</div>
<!-- Fixed time summary — shown for non-flexible slots -->
<div data-fixed-summary class="hidden text-sm text-stone-600"></div>
<!-- Cost display — shown when a slot is selected -->
<div data-cost-row class="hidden text-sm font-medium text-stone-700">
Estimated Cost: <span data-cost-display class="text-green-600">£{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }}</span>
</div>
<!-- Ticket Configuration -->
<div class="border-t pt-3 mt-3">
<h4 class="text-sm font-semibold text-stone-700 mb-3">Ticket Configuration</h4>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-sm font-medium text-stone-700 mb-1"
for="entry-ticket-price-{{ entry.id }}">
Ticket Price (£)
</label>
<input
id="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="{{ ('%.2f'|format(entry.ticket_price)) if entry.ticket_price is not none else '' }}"
>
<p class="text-xs text-stone-500 mt-1">Leave empty if no tickets needed</p>
</div>
<div>
<label class="block text-sm font-medium text-stone-700 mb-1"
for="entry-ticket-count-{{ entry.id }}">
Total Tickets
</label>
<input
id="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="{{ entry.ticket_count if entry.ticket_count is not none else '' }}"
>
<p class="text-xs text-stone-500 mt-1">Leave empty for unlimited</p>
</div>
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<!-- Cancel button -->
<button
type="button"
class="{{ styles.cancel_button }}"
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
) }}"
sx-target="#entry-{{ entry.id }}"
sx-swap="outerHTML"
>
Cancel
</button>
<!-- Save button -->
<button
type="submit"
class="{{ styles.action_button }}"
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"></i>
Save entry
</button>
</div>
</form>
</section>
{# --- Behaviour: lock / unlock times based on slot.flexible --- #}
<script>
(function () {
function timeToMinutes(timeStr) {
if (!timeStr) return 0;
const [hours, minutes] = timeStr.split(':').map(Number);
return hours * 60 + minutes;
}
function calculateCost(slotCost, slotStart, slotEnd, actualStart, actualEnd, flexible) {
if (!flexible) {
// Fixed slot: use full slot cost
return parseFloat(slotCost);
}
// Flexible slot: prorate based on time range
if (!actualStart || !actualEnd) return 0;
const slotStartMin = timeToMinutes(slotStart);
const slotEndMin = timeToMinutes(slotEnd);
const actualStartMin = timeToMinutes(actualStart);
const actualEndMin = timeToMinutes(actualEnd);
const slotDuration = slotEndMin - slotStartMin;
const actualDuration = actualEndMin - actualStartMin;
if (slotDuration <= 0 || actualDuration <= 0) return 0;
const ratio = actualDuration / slotDuration;
return parseFloat(slotCost) * ratio;
}
function initEntrySlotPicker(root) {
const select = root.querySelector('[data-slot-picker]');
if (!select) return;
const timeFields = root.querySelector('[data-time-fields]');
const startInput = root.querySelector('[data-entry-start]');
const endInput = root.querySelector('[data-entry-end]');
const helper = root.querySelector('[data-slot-boundary]');
const costDisplay = root.querySelector('[data-cost-display]');
const costRow = root.querySelector('[data-cost-row]');
const fixedSummary = root.querySelector('[data-fixed-summary]');
if (!startInput || !endInput) return;
function updateCost() {
const opt = select.selectedOptions[0];
if (!opt || !opt.value) {
if (costDisplay) costDisplay.textContent = '£0.00';
return;
}
const cost = opt.dataset.cost || '0';
const s = opt.dataset.start || '';
const e = opt.dataset.end || '';
const flexible = opt.dataset.flexible === '1';
const calculatedCost = calculateCost(
cost, s, e,
startInput.value, endInput.value,
flexible
);
if (costDisplay) {
costDisplay.textContent = '£' + calculatedCost.toFixed(2);
}
}
function applyFromOption(opt) {
if (!opt || !opt.value) {
if (timeFields) timeFields.classList.add('hidden');
if (costRow) costRow.classList.add('hidden');
if (fixedSummary) fixedSummary.classList.add('hidden');
return;
}
const s = opt.dataset.start || '';
const e = opt.dataset.end || '';
const flexible = opt.dataset.flexible === '1';
if (!flexible) {
// Fixed slot: hide time inputs, show summary + cost
if (s) startInput.value = s;
if (e) endInput.value = e;
if (timeFields) timeFields.classList.add('hidden');
if (fixedSummary) {
fixedSummary.classList.remove('hidden');
if (e) {
fixedSummary.textContent = `${s} ${e}`;
} else {
fixedSummary.textContent = `From ${s} (open-ended)`;
}
}
if (costRow) costRow.classList.remove('hidden');
} else {
// Flexible slot: show time inputs, hide fixed summary, show cost
if (timeFields) timeFields.classList.remove('hidden');
if (fixedSummary) fixedSummary.classList.add('hidden');
if (costRow) costRow.classList.remove('hidden');
if (helper) {
if (e) {
helper.textContent = `Times must be between ${s} and ${e}.`;
} else {
helper.textContent = `Start at or after ${s}.`;
}
}
}
updateCost();
}
// Initial state
applyFromOption(select.selectedOptions[0]);
select.addEventListener('change', () => {
applyFromOption(select.selectedOptions[0]);
});
// Update cost when times change (for flexible slots)
startInput.addEventListener('input', updateCost);
endInput.addEventListener('input', updateCost);
}
// Initial load
document.addEventListener('DOMContentLoaded', () => {
initEntrySlotPicker(document);
});
// HTMX fragments
if (window.htmx) {
htmx.onLoad((content) => {
initEntrySlotPicker(content);
});
}
})();
</script>

View File

@@ -1,128 +0,0 @@
<section id="entry-{{ entry.id }}" class="{{styles.list_container}}">
<!-- Entry Name -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Name
</div>
<div class="mt-1 text-lg font-medium">
{{ entry.name }}
</div>
</div>
<!-- Slot -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Slot
</div>
<div class="mt-1">
{% if entry.slot %}
<span class="px-2 py-1 rounded text-sm bg-blue-100 text-blue-700">
{{ entry.slot.name }}
</span>
{% if entry.slot.flexible %}
<span class="ml-2 text-xs text-stone-500">(flexible)</span>
{% else %}
<span class="ml-2 text-xs text-stone-500">(fixed)</span>
{% endif %}
{% else %}
<span class="text-sm text-stone-400">No slot assigned</span>
{% endif %}
</div>
</div>
<!-- Time Period -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Time Period
</div>
<div class="mt-1">
{{ entry.start_at.strftime('%H:%M') }}
{% if entry.end_at %}
{{ entry.end_at.strftime('%H:%M') }}
{% else %}
open-ended
{% endif %}
</div>
</div>
<!-- State -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
State
</div>
<div class="mt-1">
<div id="entry-state-{{entry.id}}">
{% include '_types/entry/_state.html' %}
</div>
</div>
</div>
<!-- Cost -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Cost
</div>
<div class="mt-1">
<span class="font-medium text-green-600">
£{{ ('%.2f'|format(entry.cost)) if entry.cost is not none else '0.00' }}
</span>
</div>
</div>
<!-- Ticket Configuration -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Tickets
</div>
<div class="mt-1" id="entry-tickets-{{entry.id}}">
{% include '_types/entry/_tickets.html' %}
</div>
</div>
<!-- Buy Tickets (public-facing) -->
{% include '_types/tickets/_buy_form.html' %}
<!-- Date -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Date
</div>
<div class="mt-1">
{{ entry.start_at.strftime('%A, %B %d, %Y') }}
</div>
</div>
<!-- Associated Posts -->
<div class="flex flex-col mb-4">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Associated Posts
</div>
<div class="mt-1" id="entry-posts-{{entry.id}}">
{% include '_types/entry/_posts.html' %}
</div>
</div>
<!-- Options and Edit Button -->
<div class="flex gap-2 mt-6">
{% include '_types/entry/_options.html' %}
<button
type="button"
class="{{styles.pre_action_button}}"
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.get_edit',
entry_id=entry.id,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
sx-target="#entry-{{entry.id}}"
sx-swap="outerHTML"
>
Edit
</button>
</div>
</section>

View File

@@ -1,39 +0,0 @@
{% import 'macros/links.html' as links %}
{# Associated Posts - vertical on mobile, horizontal with arrows on desktop #}
<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">
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
{% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
<a
href="{{ blog_url('/' + entry_post.slug + '/') }}"
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">
{% if entry_post.feature_image %}
<img src="{{ entry_post.feature_image }}"
alt="{{ entry_post.title }}"
class="w-8 h-8 rounded-full object-cover flex-shrink-0" />
{% else %}
<div class="w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"></div>
{% endif %}
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{{ entry_post.title }}</div>
</div>
</a>
{% endcall %}
</div>
{# Admin link #}
{% if g.rights.admin %}
{% from 'macros/admin_nav.html' import admin_nav_item %}
{{admin_nav_item(
url_for(
'calendar.day.calendar_entries.calendar_entry.admin.admin',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
)
)}}
{% endif %}

View File

@@ -1,18 +0,0 @@
{% extends "oob_elements.html" %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('day-header-child', 'entry-header-child', '_types/entry/header/_header.html')}}
{% from '_types/day/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/entry/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/entry/_main_panel.html' %}
{% endblock %}

View File

@@ -1,9 +0,0 @@
{% include '_types/entry/_options.html' %}
<div id="entry-title-{{entry.id}}" sx-swap-oob="innerHTML">
{% include '_types/entry/_title.html' %}
</div>
<div id="entry-state-{{entry.id}}" sx-swap-oob="innerHTML">
{% include '_types/entry/_state.html' %}
</div>

View File

@@ -1,95 +0,0 @@
<div id="calendar_entry_options_{{ entry.id }}" class="flex flex-col md:flex-row gap-1">
{% if entry.state == 'provisional' %}
<form
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.confirm_entry',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
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
type="submit"
data-confirm="true"
data-confirm-title="Confirm entry?"
data-confirm-text="Are you sure you want to confirm this entry?"
data-confirm-icon="question"
data-confirm-confirm-text="Yes, confirm it"
data-confirm-cancel-text="Cancel"
class="{{styles.action_button}}"
>
<i class="fa-solid fa-rotate mr-2" aria-hidden="true"></i>
confirm
</button>
</form>
<form
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.decline_entry',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
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
type="submit"
data-confirm="true"
data-confirm-title="Decline entry?"
data-confirm-text="Are you sure you want to decline this entry?"
data-confirm-icon="question"
data-confirm-confirm-text="Yes, decine it"
data-confirm-cancel-text="Cancel"
class="{{styles.action_button}}"
>
<i class="fa-solid fa-rotate mr-2" aria-hidden="true"></i>
decline
</button>
</form>
{% endif %}
{% if entry.state == 'confirmed' %}
<form
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.provisional_entry',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
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() }}">
<button
type="button"
class="{{styles.action_button}}"
data-confirm="true"
data-confirm-title="Provisional entry?"
data-confirm-text="Are you sure you want to provisional this entry?"
data-confirm-icon="question"
data-confirm-confirm-text="Yes, provisional it"
data-confirm-cancel-text="Cancel"
>
<i class="fa-solid fa-rotate mr-2" aria-hidden="true"></i>
provisional
</button>
</form>
{% endif %}
</div>

View File

@@ -1,72 +0,0 @@
{% for search_post in search_posts %}
<form
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.add_post',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
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() }}" />
<input type="hidden" name="post_id" value="{{ search_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="Add {{ search_post.title }} to this entry?"
data-confirm-icon="question"
data-confirm-confirm-text="Yes, add it"
data-confirm-cancel-text="Cancel"
>
{% if search_post.feature_image %}
<img src="{{ search_post.feature_image }}"
alt="{{ search_post.title }}"
class="w-8 h-8 rounded-full object-cover flex-shrink-0" />
{% else %}
<div class="w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"></div>
{% endif %}
<span>{{ search_post.title }}</span>
</button>
</form>
{% endfor %}
{# Infinite scroll sentinel #}
{% if page < total_pages|int %}
<div
id="post-search-sentinel-{{ page }}"
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.search_posts',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id,
q=search_query,
page=page + 1
) }}"
sx-trigger="intersect once delay:250ms"
sx-swap="outerHTML"
sx-retry="exponential:1000:30000"
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>
<div class="text-xs text-center text-stone-400 js-neterr hidden">
Connection error. Retrying...
</div>
</div>
{% elif search_posts %}
<div class="py-2 text-xs text-center text-stone-400">
End of results
</div>
{% endif %}

View File

@@ -1,72 +0,0 @@
<!-- Associated Posts Section -->
<div class="space-y-2">
{% if entry_posts %}
<div class="space-y-2">
{% for entry_post in entry_posts %}
<div class="flex items-center justify-between gap-3 p-2 bg-stone-50 rounded border">
{% if entry_post.feature_image %}
<img src="{{ entry_post.feature_image }}"
alt="{{ entry_post.title }}"
class="w-8 h-8 rounded-full object-cover flex-shrink-0" />
{% else %}
<div class="w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"></div>
{% endif %}
<span class="text-sm flex-1">{{ entry_post.title }}</span>
<button
type="button"
class="text-xs text-red-600 hover:text-red-800 flex-shrink-0"
data-confirm
data-confirm-title="Remove post?"
data-confirm-text="This will remove {{ entry_post.title }} from this entry"
data-confirm-icon="warning"
data-confirm-confirm-text="Yes, remove it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
sx-delete="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.remove_post',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id,
post_id=entry_post.id
) }}"
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>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-sm text-stone-400">No posts associated</p>
{% endif %}
<!-- Search to add posts -->
<div class="mt-3 pt-3 border-t">
<label class="block text-xs font-medium text-stone-700 mb-1">
Add Post
</label>
<input
type="text"
placeholder="Search posts..."
class="w-full px-3 py-2 border rounded text-sm"
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.search_posts',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
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>
</div>
</div>

View File

@@ -1,15 +0,0 @@
{% if entry.state %}
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium
{% if entry.state == 'confirmed' %}
bg-emerald-100 text-emerald-800
{% elif entry.state == 'provisional' %}
bg-amber-100 text-amber-800
{% elif entry.state == 'ordered' %}
bg-blue-100 text-blue-800
{% else %}
bg-stone-100 text-stone-700
{% endif %}
">
{{ entry.state|capitalize }}
</span>
{% endif %}

View File

@@ -1,104 +0,0 @@
{% if entry.ticket_price is not none %}
{# Tickets are configured #}
<div class="space-y-2">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-stone-700">Price:</span>
<span class="font-medium text-green-600">
£{{ ('%.2f'|format(entry.ticket_price)) }}
</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-stone-700">Available:</span>
<span class="font-medium text-blue-600">
{% if entry.ticket_count is not none %}
{{ entry.ticket_count }} tickets
{% else %}
Unlimited
{% endif %}
</span>
</div>
<button
type="button"
class="text-xs text-blue-600 hover:text-blue-800 underline"
onclick="document.getElementById('ticket-form-{{entry.id}}').classList.remove('hidden'); this.classList.add('hidden');"
>
Edit ticket config
</button>
</div>
{% else %}
{# No tickets configured #}
<div class="space-y-2">
<span class="text-sm text-stone-400">No tickets configured</span>
<button
type="button"
class="block text-xs text-blue-600 hover:text-blue-800 underline"
onclick="document.getElementById('ticket-form-{{entry.id}}').classList.remove('hidden'); this.classList.add('hidden');"
>
Configure tickets
</button>
</div>
{% endif %}
{# Ticket configuration form (hidden by default) #}
<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"
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.update_tickets',
entry_id=entry.id,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
sx-target="#entry-tickets-{{entry.id}}"
sx-swap="innerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div>
<label for="ticket-price-{{entry.id}}" class="block text-sm font-medium text-stone-700 mb-1">
Ticket Price (£)
</label>
<input
type="number"
id="ticket-price-{{entry.id}}"
name="ticket_price"
step="0.01"
min="0"
value="{{ ('%.2f'|format(entry.ticket_price)) if entry.ticket_price is not none else '' }}"
class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g., 5.00"
/>
</div>
<div>
<label for="ticket-count-{{entry.id}}" class="block text-sm font-medium text-stone-700 mb-1">
Total Tickets
</label>
<input
type="number"
id="ticket-count-{{entry.id}}"
name="ticket_count"
min="0"
value="{{ entry.ticket_count if entry.ticket_count is not none else '' }}"
class="w-full px-3 py-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Leave empty for unlimited"
/>
</div>
<div class="flex gap-2">
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm"
>
Save
</button>
<button
type="button"
class="px-4 py-2 bg-stone-200 text-stone-700 rounded hover:bg-stone-300 text-sm"
onclick="document.getElementById('ticket-form-{{entry.id}}').classList.add('hidden'); document.getElementById('entry-tickets-{{entry.id}}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));"
>
Cancel
</button>
</div>
</form>

View File

@@ -1,5 +0,0 @@
{% from 'macros/date.html' import t %}
<div class="text-sm text-gray-600">
{{ t(entry.start_at) }}
{% if entry.end_at %} → {{ t(entry.end_at) }}{% endif %}
</div>

View File

@@ -1,3 +0,0 @@
<i class="fa fa-clock"></i>
{{ entry.name }}
{% include '_types/entry/_state.html' %}

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,17 +0,0 @@
{% import 'macros/links.html' as links %}
{% call links.link(
url_for(
'calendar.day.calendar_entries.calendar_entry.ticket_types.get',
calendar_slug=calendar.slug,
entry_id=entry.id,
year=year,
month=month,
day=day
),
hx_select_search,
select_colours,
True,
aclass=styles.nav_button,
)%}
ticket_types
{% endcall %}

View File

@@ -1,31 +0,0 @@
{# OOB swap for entry posts nav when posts are associated/disassociated #}
{% import 'macros/links.html' as links %}
{# Associated Posts - vertical on mobile, horizontal with arrows on desktop #}
{% 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"
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
href="{{ blog_url('/' + entry_post.slug + '/') }}"
class="{{styles.nav_button}}"
>
{% if entry_post.feature_image %}
<img src="{{ entry_post.feature_image }}"
alt="{{ entry_post.title }}"
class="w-8 h-8 rounded-full object-cover flex-shrink-0" />
{% else %}
<div class="w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"></div>
{% endif %}
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{{ entry_post.title }}</div>
</div>
</a>
{% endcall %}
</div>
{% else %}
{# Empty placeholder to remove nav posts when all are disassociated #}
<div id="entry-posts-nav-wrapper" sx-swap-oob="true"></div>
{% endif %}

View File

@@ -1,25 +0,0 @@
{% extends 'oob_elements.html' %}
{# OOB elements for calendar admin page #}
{# Import shared OOB macros #}
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('entry-header-child', 'entry-admin-header-child', '_types/entry/admin/header/_header.html')}}
{% from '_types/entry/header/_header.html' import header_row with context %}
{{header_row(oob=True)}}
{% endblock %}
{% block mobile_menu %}
{% include '_types/entry/admin/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/entry/admin/_main_panel.html' %}
{% endblock %}

View File

@@ -1,21 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='entry-admin-row', oob=oob) %}
{% call links.link(
url_for(
'calendar.day.calendar_entries.calendar_entry.admin.admin',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
),
hx_select_search
) %}
{{ links.admin() }}
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/entry/admin/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,24 +0,0 @@
{% extends '_types/entry/index.html' %}
{% import 'macros/layout.html' as layout %}
{% import 'macros/links.html' as links %}
{% block entry_header_child %}
{% from '_types/root/_n/macros.html' import header with context %}
{% call header() %}
{% from '_types/entry/admin/header/_header.html' import header_row with context %}
{{ header_row() }}
<div id="entry-admin-header-child">
{% block entry_admin_header_child %}
{% endblock %}
</div>
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/entry/admin/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/entry/admin/_main_panel.html' %}
{% endblock %}

View File

@@ -1,27 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='entry-row', oob=oob) %}
{% call links.link(
url_for(
'calendar.day.calendar_entries.calendar_entry.get',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
),
hx_select_search,
) %}
<div id="entry-title-{{entry.id}}" class="flex gap-1 items-center">
{% include '_types/entry/_title.html' %}
{% include '_types/entry/_times.html' %}
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/entry/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,20 +0,0 @@
{% extends '_types/day/index.html' %}
{% block day_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('entry-header-child', '_types/entry/header/_header.html') %}
{% block entry_header_child %}
{% endblock %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/entry/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/entry/_main_panel.html' %}
{% endblock %}

View File

@@ -1,25 +0,0 @@
<section class="p-4">
{% if has_access('markets.create_market') %}
<div id="market-create-errors" class="mt-2 text-sm text-red-600"></div>
<form
class="mt-4 flex gap-2 items-end"
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">
<label class="block text-sm text-gray-600">Name</label>
<input name="name" type="text" required class="w-full border rounded px-3 py-2" placeholder="e.g. Farm Shop, Bakery" />
</div>
<button type="submit" class="border rounded px-3 py-2">Add market</button>
</form>
{% endif %}
<div id="markets-list" class="mt-6">
{% include "_types/markets/_markets_list.html" %}
</div>
</section>

View File

@@ -1,37 +0,0 @@
{% for m in markets %}
<div class="mt-6 border rounded-lg p-4">
<div class="flex items-center justify-between gap-3">
{% set market_href = market_url('/' + post.slug + '/' + m.slug + '/') %}
<a
class="flex items-baseline gap-3"
href="{{ market_href }}"
>
<h3 class="font-semibold">{{ m.name }}</h3>
<h4 class="text-gray-500">/{{ m.slug }}/</h4>
</a>
<button
class="text-sm border rounded px-3 py-1 hover:bg-red-50 hover:border-red-400"
data-confirm
data-confirm-title="Delete market?"
data-confirm-text="Products will be hidden (soft delete)"
data-confirm-icon="warning"
data-confirm-confirm-text="Yes, delete it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
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>
</div>
</div>
{% else %}
<p class="text-gray-500 mt-4">No markets yet. Create one above.</p>
{% endfor %}

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,19 +0,0 @@
{% extends 'oob_elements.html' %}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('post-admin-header-child', 'markets-header-child', '_types/markets/header/_header.html')}}
{% from '_types/post/admin/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/markets/_nav.html' %}
{% endblock %}
{% block content %}
{% include "_types/markets/_main_panel.html" %}
{% endblock %}

View File

@@ -1,14 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='markets-row', oob=oob) %}
{% call links.link(url_for('markets.defpage_events_markets'), hx_select_search) %}
<i class="fa fa-shopping-bag" aria-hidden="true"></i>
<div>
Markets
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/markets/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,23 +0,0 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block root_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
{% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
{% call index_row('markets-header-child', '_types/markets/header/_header.html') %}
{% block markets_header_child %}
{% endblock %}
{% endcall %}
{% endcall %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/markets/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/markets/_main_panel.html' %}
{% endblock %}

View File

@@ -1,49 +0,0 @@
{# List card for page summary — one entry #}
{% set pi = page_info.get(entry.calendar_container_id, {}) %}
{% set page_slug = pi.get('slug', post.slug) %}
{% set page_title = pi.get('title') %}
<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">
{# Left: event info #}
<div class="flex-1 min-w-0">
{% set day_href = events_url('/' ~ page_slug ~ '/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %}
{% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' %}
<a href="{{ entry_href }}" class="hover:text-emerald-700">
<h2 class="text-lg font-semibold text-stone-900">{{ entry.name }}</h2>
</a>
<div class="flex flex-wrap items-center gap-1.5 mt-1">
{% if page_title and page_title != post.title %}
<a href="{{ events_url('/' ~ page_slug ~ '/') }}"
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200">
{{ page_title }}
</a>
{% endif %}
{% if entry.calendar_name %}
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700">
{{ entry.calendar_name }}
</span>
{% endif %}
</div>
<div class="mt-1 text-sm text-stone-500">
{{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} &ndash; {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
{% if entry.cost %}
<div class="mt-1 text-sm font-medium text-green-600">
&pound;{{ '%.2f'|format(entry.cost) }}
</div>
{% endif %}
</div>
{# Right: ticket widget #}
{% if entry.ticket_price is not none %}
<div class="shrink-0">
{% set qty = pending_tickets.get(entry.id, 0) %}
{% set ticket_url = url_for('page_summary.adjust_ticket') %}
{% include '_types/page_summary/_ticket_widget.html' %}
</div>
{% endif %}
</div>
</article>

View File

@@ -1,48 +0,0 @@
{# Tile card for page summary — compact event tile #}
{% set pi = page_info.get(entry.calendar_container_id, {}) %}
{% set page_slug = pi.get('slug', post.slug) %}
{% set page_title = pi.get('title') %}
<article class="rounded-xl bg-white shadow-sm border border-stone-200 overflow-hidden">
{% set day_href = events_url('/' ~ page_slug ~ '/' ~ entry.calendar_slug ~ '/day/' ~ entry.start_at.strftime('%Y/%-m/%-d') ~ '/') %}
{% set entry_href = day_href ~ 'entries/' ~ entry.id ~ '/' %}
<div class="p-3">
<a href="{{ entry_href }}" class="hover:text-emerald-700">
<h2 class="text-base font-semibold text-stone-900 line-clamp-2">{{ entry.name }}</h2>
</a>
<div class="flex flex-wrap items-center gap-1 mt-1">
{% if page_title and page_title != post.title %}
<a href="{{ events_url('/' ~ page_slug ~ '/') }}"
class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200">
{{ page_title }}
</a>
{% endif %}
{% if entry.calendar_name %}
<span class="inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-sky-100 text-sky-700">
{{ entry.calendar_name }}
</span>
{% endif %}
</div>
<div class="mt-1 text-xs text-stone-500">
<a href="{{ day_href }}" class="hover:text-stone-700">{{ entry.start_at.strftime('%a %-d %b') }}</a>
&middot;
{{ entry.start_at.strftime('%H:%M') }}{% if entry.end_at %} &ndash; {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
{% if entry.cost %}
<div class="mt-1 text-sm font-medium text-green-600">
&pound;{{ '%.2f'|format(entry.cost) }}
</div>
{% endif %}
</div>
{# Ticket widget below card #}
{% if entry.ticket_price is not none %}
<div class="border-t border-stone-100 px-3 py-2">
{% set qty = pending_tickets.get(entry.id, 0) %}
{% set ticket_url = url_for('page_summary.adjust_ticket') %}
{% include '_types/page_summary/_ticket_widget.html' %}
</div>
{% endif %}
</article>

View File

@@ -1,31 +0,0 @@
{% for entry in entries %}
{% if view == 'tile' %}
{% include "_types/page_summary/_card_tile.html" %}
{% else %}
{# Date header when date changes (list view only) #}
{% set entry_date = entry.start_at.strftime('%A %-d %B %Y') %}
{% if loop.first or entry_date != entries[loop.index0 - 1].start_at.strftime('%A %-d %B %Y') %}
<div class="pt-2 pb-1">
<h3 class="text-sm font-semibold text-stone-500 uppercase tracking-wide">
{{ entry_date }}
</h3>
</div>
{% endif %}
{% include "_types/page_summary/_card.html" %}
{% endif %}
{% endfor %}
{% if has_more %}
{# Infinite scroll sentinel #}
{% set entries_url = url_for('page_summary.entries_fragment', page=page + 1, view=view if view != 'list' else '')|host %}
<div
id="sentinel-{{ page }}"
class="h-4 opacity-0 pointer-events-none"
sx-get="{{ entries_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...</div>
</div>
{% endif %}

View File

@@ -1,54 +0,0 @@
{# View toggle bar - desktop only #}
<div class="hidden md:flex justify-end px-3 pt-3 gap-1">
{% set list_href = (current_local_href ~ {'view': None}|qs)|host %}
{% set tile_href = (current_local_href ~ {'view': 'tile'}|qs)|host %}
<a
href="{{ list_href }}"
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"
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" />
</svg>
</a>
<a
href="{{ tile_href }}"
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"
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" />
</svg>
</a>
</div>
{# Cards container - list or grid based on view #}
{% if entries %}
{% if view == 'tile' %}
<div class="max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{% include "_types/page_summary/_cards.html" %}
</div>
{% else %}
<div class="max-w-full px-3 py-3 space-y-3">
{% include "_types/page_summary/_cards.html" %}
</div>
{% endif %}
{% else %}
<div class="px-3 py-12 text-center text-stone-400">
<i class="fa fa-calendar-xmark text-4xl mb-3" aria-hidden="true"></i>
<p class="text-lg">No upcoming events</p>
</div>
{% endif %}
<div class="pb-8"></div>

View File

@@ -1,63 +0,0 @@
{# Inline ticket +/- widget for page summary cards.
Variables: entry, qty, ticket_url
Wrapped in a div with stable ID for HTMX targeting. #}
<div id="page-ticket-{{ entry.id }}" class="flex items-center gap-2">
<span class="text-green-600 font-medium text-sm">&pound;{{ '%.2f'|format(entry.ticket_price) }}</span>
{% if qty == 0 %}
<form
action="{{ ticket_url }}"
method="post"
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 }}">
<input type="hidden" name="count" value="1">
<button
type="submit"
class="relative inline-flex items-center justify-center text-stone-500 hover:bg-emerald-50 rounded p-1"
>
<i class="fa fa-cart-plus text-2xl" aria-hidden="true"></i>
</button>
</form>
{% else %}
<form
action="{{ ticket_url }}"
method="post"
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 }}">
<input type="hidden" name="count" value="{{ qty - 1 }}">
<button type="submit"
class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl">-</button>
</form>
<a class="relative inline-flex items-center justify-center text-emerald-700" href="{{ cart_url('/') }}">
<span class="relative inline-flex items-center justify-center">
<i class="fa-solid fa-shopping-cart text-xl" aria-hidden="true"></i>
<span class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none">
<span class="flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold">{{ qty }}</span>
</span>
</span>
</a>
<form
action="{{ ticket_url }}"
method="post"
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 }}">
<input type="hidden" name="count" value="{{ qty + 1 }}">
<button type="submit"
class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl">+</button>
</form>
{% endif %}
</div>

View File

@@ -1,15 +0,0 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block root_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
{% block post_header_child %}
{% endblock %}
{% endcall %}
{% endblock %}
{% block content %}
{% include '_types/page_summary/_main_panel.html' %}
{% endblock %}

View File

@@ -1,70 +0,0 @@
<section class="p-4 max-w-lg mx-auto">
<div id="payments-panel" class="space-y-4 p-4 bg-white rounded-lg border border-stone-200">
<h3 class="text-lg font-semibold text-stone-800">
<i class="fa fa-credit-card text-purple-600 mr-1"></i>
SumUp Payment
</h3>
<p class="text-xs text-stone-400">
Configure per-page SumUp credentials. Leave blank to use the global merchant account.
</p>
<form
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() }}">
<div>
<label class="block text-xs font-medium text-stone-600 mb-1">Merchant Code</label>
<input
type="text"
name="merchant_code"
value="{{ sumup_merchant_code }}"
placeholder="e.g. ME4J6100"
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
>
</div>
<div>
<label class="block text-xs font-medium text-stone-600 mb-1">API Key</label>
<input
type="password"
name="api_key"
value=""
placeholder="{{ '--------' if sumup_configured else 'sup_sk_...' }}"
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
>
{% if sumup_configured %}
<p class="text-xs text-stone-400 mt-0.5">Key is set. Leave blank to keep current key.</p>
{% endif %}
</div>
<div>
<label class="block text-xs font-medium text-stone-600 mb-1">Checkout Reference Prefix</label>
<input
type="text"
name="checkout_prefix"
value="{{ sumup_checkout_prefix }}"
placeholder="e.g. ROSE-"
class="w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
>
</div>
<button
type="submit"
class="px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500"
>
Save SumUp Settings
</button>
{% if sumup_configured %}
<span class="ml-2 text-xs text-green-600">
<i class="fa fa-check-circle"></i> Connected
</span>
{% endif %}
</form>
</div>
</section>

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,19 +0,0 @@
{% extends 'oob_elements.html' %}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('post-admin-header-child', 'payments-header-child', '_types/payments/header/_header.html')}}
{% from '_types/post/admin/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/payments/_nav.html' %}
{% endblock %}
{% block content %}
{% include "_types/payments/_main_panel.html" %}
{% endblock %}

View File

@@ -1,14 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='payments-row', oob=oob) %}
{% call links.link(url_for('payments.home'), hx_select_search) %}
<i class="fa fa-credit-card" aria-hidden="true"></i>
<div>
Payments
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/payments/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,23 +0,0 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block root_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('post-header-child', '_types/post/header/_header.html') %}
{% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
{% call index_row('payments-header-child', '_types/payments/header/_header.html') %}
{% block payments_header_child %}
{% endblock %}
{% endcall %}
{% endcall %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/payments/_nav.html' %}
{% endblock %}
{% block content %}
{% include '_types/payments/_main_panel.html' %}
{% endblock %}

View File

@@ -1,14 +0,0 @@
{% import 'macros/links.html' as links %}
{% if calendars %}
{% for calendar in calendars %}
{% call links.link(url_for('calendar.get', calendar_slug=calendar.slug), hx_select_search, select_colours, True, aclass=styles.nav_button_less_pad) %}
<i class="fa fa-calendar" aria-hidden="true"></i>
<div>{{ calendar.name }}</div>
{% endcall %}
{% endfor %}
{% endif %}
{% if g.rights.admin %}
<a href="{{ blog_url('/' + post.slug + '/admin/') }}" class="{{styles.nav_button}}">
<i class="fa fa-cog" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -1,50 +0,0 @@
<div id="associated-entries-list" class="border rounded-lg p-4 bg-white">
<h3 class="text-lg font-semibold mb-4">Associated Entries</h3>
{% if associated_entry_ids %}
<div class="space-y-1">
{% for calendar in all_calendars %}
{% for entry in calendar.entries %}
{% if entry.id in associated_entry_ids and entry.deleted_at is none %}
<button
type="button"
class="w-full text-left p-3 rounded border bg-green-50 border-green-300 transition hover:bg-green-100"
data-confirm
data-confirm-title="Remove entry?"
data-confirm-text="This will remove {{ entry.name }} from this post"
data-confirm-icon="warning"
data-confirm-confirm-text="Yes, remove it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
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 %}
<img src="{{ calendar.post.feature_image }}"
alt="{{ calendar.post.title }}"
class="w-8 h-8 rounded-full object-cover flex-shrink-0" />
{% else %}
<div class="w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"></div>
{% endif %}
<div class="flex-1">
<div class="font-medium text-sm">{{ entry.name }}</div>
<div class="text-xs text-stone-600 mt-1">
{{ calendar.name }} • {{ entry.start_at.strftime('%A, %B %d, %Y at %H:%M') }}
{% if entry.end_at %} {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
</div>
<i class="fa fa-times-circle text-green-600 text-lg flex-shrink-0"></i>
</div>
</button>
{% endif %}
{% endfor %}
{% endfor %}
</div>
{% else %}
<div class="text-sm text-stone-400">No entries associated yet. Browse calendars below to add entries.</div>
{% endif %}
</div>

View File

@@ -1,36 +0,0 @@
{% import 'macros/links.html' as links %}
<div class="relative nav-group">
<a href="{{ events_url('/' + post.slug + '/calendar/') }}" class="{{styles.nav_button}}">
calendar
</a>
</div>
<div class="relative nav-group">
<a href="{{ market_url('/' + post.slug + '/') }}" class="{{styles.nav_button}}">
markets
</a>
</div>
<div class="relative nav-group">
<a href="{{ events_url('/' + post.slug + '/payments/') }}" class="{{styles.nav_button}}">
payments
</a>
</div>
<div class="relative nav-group">
<a href="{{ blog_url('/' + post.slug + '/admin/entries/') }}" class="{{styles.nav_button}}">
entries
</a>
</div>
<div class="relative nav-group">
<a href="{{ blog_url('/' + post.slug + '/admin/data/') }}" class="{{styles.nav_button}}">
data
</a>
</div>
<div class="relative nav-group">
<a href="{{ blog_url('/' + post.slug + '/admin/edit/') }}" class="{{styles.nav_button}}">
edit
</a>
</div>
<div class="relative nav-group">
<a href="{{ blog_url('/' + post.slug + '/admin/settings/') }}" class="{{styles.nav_button}}">
settings
</a>
</div>

View File

@@ -1,34 +0,0 @@
{# OOB swap for nav entries and calendars when toggling associations or editing calendars #}
{% from 'macros/nav_entries.html' import nav_entries_oob %}
{% set has_items = (associated_entries and associated_entries.entries) or calendars %}
{% call nav_entries_oob(has_items) %}
{% if associated_entries and associated_entries.entries %}
{% for entry in associated_entries.entries %}
{% set _entry_path = '/' + post.slug + '/' +entry.calendar_slug + '/' + entry.start_at.year|string + '/' + entry.start_at.month|string + '/' + entry.start_at.day|string + '/entries/' + entry.id|string + '/' %}
<a
href="{{ events_url(_entry_path) }}"
class="{{styles.nav_button_less_pad}}">
<div class="w-8 h-8 rounded bg-stone-200 flex-shrink-0"></div>
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{{ entry.name }}</div>
<div class="text-xs text-stone-600 truncate">
{{ entry.start_at.strftime('%b %d, %Y at %H:%M') }}
{% if entry.end_at %} {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
</div>
</a>
{% endfor %}
{% endif %}
{% if calendars %}
{% for calendar in calendars %}
{% set local_href=events_url('/' + post.slug + '/' +calendar.slug + '/') %}
<a
href="{{ local_href }}"
class="{{styles.nav_button_less_pad}}">
<i class="fa fa-calendar" aria-hidden="true"></i>
<div>{{calendar.name}}</div>
</a>
{% endfor %}
{% endif %}
{% endcall %}

View File

@@ -1,12 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='post-admin-row', oob=oob) %}
<a href="{{ blog_url('/' + post.slug + '/admin/') }}"
class="flex items-center gap-2 px-3 py-2 rounded">
{{ links.admin() }}
</a>
{% call links.desktop_nav() %}
{% include '_types/post/admin/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,28 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='post-row', oob=oob) %}
{% call links.link(blog_url('/' + post.slug + '/'), hx_select_search ) %}
{% if post.feature_image %}
<img
src="{{ post.feature_image }}"
class="h-8 w-8 rounded-full object-cover border border-stone-300 flex-shrink-0"
>
{% endif %}
<span>
{{ post.title | truncate(160, True, '…') }}
</span>
{% endcall %}
{% call links.desktop_nav() %}
{% if page_cart_count is defined and page_cart_count > 0 %}
<a
href="{{ cart_url('/' + post.slug + '/') }}"
class="relative inline-flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-full border border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100 transition"
>
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
<span>{{ page_cart_count }}</span>
</a>
{% endif %}
{% include '_types/post/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,40 +0,0 @@
<div id="post-entries-content" class="space-y-6 p-4">
{# Associated Entries List #}
{% include '_types/post/admin/_associated_entries.html' %}
{# Calendars Browser #}
<div class="space-y-3">
<h3 class="text-lg font-semibold">Browse Calendars</h3>
{% for calendar in all_calendars %}
<details class="border rounded-lg bg-white"
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 }}"
alt="{{ calendar.post.title }}"
class="w-12 h-12 rounded object-cover flex-shrink-0" />
{% else %}
<div class="w-12 h-12 rounded bg-stone-200 flex-shrink-0"></div>
{% endif %}
<div class="flex-1">
<div class="font-semibold flex items-center gap-2">
<i class="fa fa-calendar text-stone-500"></i>
{{ calendar.name }}
</div>
<div class="text-sm text-stone-600">
{{ calendar.post.title }}
</div>
</div>
</summary>
<div class="p-4 border-t"
sx-trigger="intersect once"
sx-swap="innerHTML">
<div class="text-sm text-stone-400">Loading calendar...</div>
</div>
</details>
{% else %}
<div class="text-sm text-stone-400">No calendars found.</div>
{% endfor %}
</div>
</div>

View File

@@ -1,2 +0,0 @@
{% from 'macros/admin_nav.html' import placeholder_nav %}
{{ placeholder_nav() }}

View File

@@ -1,17 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='post_entries-row', oob=oob) %}
{% call links.link(blog_url('/' + post.slug + '/admin/entries/'), hx_select_search) %}
<i class="fa fa-clock" aria-hidden="true"></i>
<div>
entries
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/post_entries/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,13 +0,0 @@
{% macro description(slot, oob=False) %}
<div
id="slot-description-title"
{% if oob %}
sx-swap-oob="outerHTML"
{% endif %}
class="text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
>
{{ slot.description or ''}}
</div>
{% endmacro %}

View File

@@ -1,5 +0,0 @@
<p class="text-stone-500 whitespace-pre-line break-all w-full">
{% if slot.description %}
{{ slot.description }}
{% endif %}
</p>

View File

@@ -1,180 +0,0 @@
<section id="slot-{{ slot.id }}" class="{{styles.list_container}}">
<!-- Quick-edit form -->
<div id="slot-errors" class="mt-2 text-sm text-red-600"></div>
<form
class="space-y-3 mt-4"
sx-put="{{ url_for('calendar.slots.slot.put',
calendar_slug=calendar.slug,
slot_id=slot.id) }}"
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() }}">
<!-- Name -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1" for="slot-name-{{ slot.id }}">
Name
</label>
<input
id="slot-name-{{ slot.id }}"
name="name"
placeholder="Name"
class="w-full border p-2 rounded"
value="{{ slot.name }}"
/>
</div>
<!-- Cost -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1" for="slot-cost-{{ slot.id }}">
Cost
</label>
<input
id="slot-cost-{{ slot.id }}"
name="cost"
placeholder="Cost e.g. 12.50"
class="w-full border p-2 rounded"
value="{{ '%.2f'|format(slot.cost) if slot.cost is not none else '' }}"
/>
</div>
<!-- Time start -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1" for="slot-start-{{ slot.id }}">
Start time
</label>
<input
id="slot-start-{{ slot.id }}"
name="time_start"
placeholder="Start HH:MM"
class="w-full border p-2 rounded"
value="{{ slot.time_start.strftime('%H:%M') if slot.time_start else '' }}"
/>
</div>
<!-- Time end -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1" for="slot-end-{{ slot.id }}">
End time
</label>
<input
id="slot-end-{{ slot.id }}"
name="time_end"
placeholder="End HH:MM"
class="w-full border p-2 rounded"
value="{{ slot.time_end.strftime('%H:%M') if slot.time_end else '' }}"
/>
</div>
<!-- Description -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1" for="slot-desc-{{ slot.id }}">
Description
</label>
<textarea
id="slot-desc-{{ slot.id }}"
name="description"
rows="2"
placeholder="Description"
class="w-full border p-2 rounded"
>{{ slot.description or '' }}</textarea>
</div>
<!-- Days -->
<div>
<span class="block text-sm font-medium text-stone-700 mb-1">
Days
</span>
{# pre-check "All" if every day is true on this slot #}
{% set all_days_checked =
slot|getattr('mon')
and slot|getattr('tue')
and slot|getattr('wed')
and slot|getattr('thu')
and slot|getattr('fri')
and slot|getattr('sat')
and slot|getattr('sun') %}
<div
class="flex flex-wrap gap-3 items-center text-sm"
data-days-group
>
{# "All" toggle no name so its not submitted #}
<label class="flex items-center gap-1 px-2 py-1 rounded-full bg-slate-200">
<input
type="checkbox"
data-day-all
{% if all_days_checked %}checked{% endif %}
/>
<span>All</span>
</label>
{# Individual days, with data-day like the add form #}
{% for key, label in [
('mon','Mon'),('tue','Tue'),('wed','Wed'),('thu','Thu'),
('fri','Fri'),('sat','Sat'),('sun','Sun')
] %}
{% set is_checked = slot|getattr(key) %}
<label class="flex items-center gap-1 px-2 py-1 rounded-full bg-slate-100">
<input
type="checkbox"
name="{{ key }}"
value="1"
data-day="{{ key }}"
{% if is_checked %}checked{% endif %}
/>
<span>{{ label }}</span>
</label>
{% endfor %}
</div>
</div>
<!-- NEW: Flexible flag -->
<div>
<label class="block text-sm font-medium text-stone-700 mb-1" for="slot-flexible-{{ slot.id }}">
Flexible booking
</label>
<label class="inline-flex items-center gap-2 text-xs">
<input
id="slot-flexible-{{ slot.id }}"
type="checkbox"
name="flexible"
value="1"
{% if slot.flexible %}checked{% endif %}
>
<span>Allow bookings at any time within this band</span>
</label>
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="button"
class="{{styles.cancel_button}}"
sx-get="{{ url_for('calendar.slots.slot.get_view',
calendar_slug=calendar.slug,
slot_id=slot.id) }}"
sx-target="#slot-{{ slot.id }}"
sx-swap="outerHTML"
>
Cancel
</button>
<button
type="submit"
class="{{ styles.action_button }}"
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"></i>
Save slot
</button>
</div>
</form>
</section>

View File

@@ -1,72 +0,0 @@
<section id="slot-{{slot.id}}" class="{{styles.list_container}}">
<!-- Days -->
<div class="flex flex-col">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Days
</div>
<div class="mt-1">
{% set days = slot.days_display.split(', ') %}
{% if days and days[0] != "—" %}
<div class="flex flex-wrap gap-1">
{% for day in days %}
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-200">
{{ day }}
</span>
{% endfor %}
</div>
{% else %}
<span class="text-xs text-slate-400">No days</span>
{% endif %}
</div>
</div>
<!-- Flexible -->
<div class="flex flex-col">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Flexible
</div>
<div class="mt-1">
{{ 'yes' if slot.flexible else 'no' }}
</div>
</div>
<!-- Time & Cost (still "up-down" per field, but can sit side-by-side on wide screens) -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div class="flex flex-col">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Time
</div>
<div class="mt-1">
{{ slot.time_start.strftime('%H:%M') }} — {{ slot.time_end.strftime('%H:%M') }}
</div>
</div>
<div class="flex flex-col">
<div class="text-xs font-semibold uppercase tracking-wide text-stone-500">
Cost
</div>
<div class="mt-1">
{{ ('%.2f'|format(slot.cost)) if slot.cost is not none else '' }}
</div>
</div>
</div>
<button
type="button"
class="{{styles.pre_action_button}}"
sx-get="{{ url_for(
'calendar.slots.slot.get_edit',
slot_id=slot.id,
calendar_slug=calendar.slug,
) }}"
sx-target="#slot-{{slot.id}}"
sx-swap="outerHTML"
>
Edit
</button>
</section>
{% if oob %}
{% from '_types/slot/__description.html' import description %}
{{description(slot, oob=True)}}
{% endif %}

View File

@@ -1,15 +0,0 @@
{% extends "oob_elements.html" %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('slots-header-child', 'slot-header-child', '_types/slot/header/_header.html')}}
{% from '_types/slots/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block content %}
{% include '_types/slot/_main_panel.html' %}
{% endblock %}

View File

@@ -1,25 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='slot-row', oob=oob) %}
{% call links.link(
hx_select_search,
) %}
<div class="flex flex-col md:flex-row md:gap-2 items-center">
<div class="flex flex-row items-center gap-2">
<i class="fa fa-clock"></i>
<div class="shrink-0">
{{ slot.name }}
</div>
</div>
{% from '_types/slot/__description.html' import description %}
{{description(slot)}}
</div>
{% endcall %}
{% call links.desktop_nav() %}
{#% include '_types/slot/_nav.html' %#}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,20 +0,0 @@
{% extends '_types/slots/index.html' %}
{% import 'macros/layout.html' as layout %}
{% block slots_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('slot-header-child', '_types/slot/header/_header.html') %}
{% block slot_header_child %}
{% endblock %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{#% include '_types/slot/_nav.html' %#}
{% endblock %}
{% block content %}
{% include '_types/slot/_main_panel.html' %}
{% endblock %}

View File

@@ -1,123 +0,0 @@
<form
sx-post="{{ url_for('calendar.slots.post',
calendar_slug=calendar.slug) }}"
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">
<div class="md:col-span-2">
<label class="block text-xs font-semibold mb-1">Name</label>
<input
type="text"
name="name"
class="w-full border rounded px-2 py-1 text-sm"
required
>
</div>
<div class="md:col-span-2">
<label class="block text-xs font-semibold mb-1">Description</label>
<input
type="text"
name="description"
class="w-full border rounded px-2 py-1 text-sm"
>
</div>
<div>
<label class="block text-xs font-semibold mb-1">Days</label>
<div class="flex flex-wrap gap-1 text-xs" data-days-group>
{# "All" toggle no name so its not submitted #}
<label class="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-slate-200">
<input type="checkbox" data-day-all>
<span>All</span>
</label>
{# Individual days #}
{% for key, label in [
('mon','Mon'),('tue','Tue'),('wed','Wed'),('thu','Thu'),
('fri','Fri'),('sat','Sat'),('sun','Sun')
] %}
<label class="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-slate-100">
<input type="checkbox" name="{{ key }}" value="1" data-day="{{ key }}">
<span>{{ label }}</span>
</label>
{% endfor %}
</div>
</div>
<div>
<label class="block text-xs font-semibold mb-1">Time start</label>
<input
type="time"
name="time_start"
class="w-full border rounded px-2 py-1 text-sm"
required
>
</div>
<div>
<label class="block text-xs font-semibold mb-1">Time end</label>
<input
type="time"
name="time_end"
class="w-full border rounded px-2 py-1 text-sm"
required
>
</div>
<div>
<label class="block text-xs font-semibold mb-1">Cost</label>
<input
type="text"
name="cost"
class="w-full border rounded px-2 py-1 text-sm"
placeholder="e.g. 5.00"
>
</div>
{# NEW: flexible flag #}
<div class="md:col-span-2">
<label class="block text-xs font-semibold mb-1">Flexible booking</label>
<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</span>
</label>
</div>
</div>
<div class="flex justify-end gap-2 pt-2">
<button
type="button"
class="{{styles.cancel_button}}"
sx-get="{{ url_for('calendar.slots.add_button',
calendar_slug=calendar.slug) }}"
sx-target="#slot-add-container"
sx-swap="innerHTML"
>
Cancel
</button>
<button
type="submit"
class="{{styles.action_button}}"
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"></i>
Save slot
</button>
</div>
</form>

View File

@@ -1,11 +0,0 @@
<button
type="button"
class="{{styles.pre_action_button}}"
sx-get="{{ url_for('calendar.slots.add_form',
calendar_slug=calendar.slug) }}"
sx-target="#slot-add-container"
sx-swap="innerHTML"
>
+ Add slot
</button>

View File

@@ -1,26 +0,0 @@
<section id="slots-table" class="{{styles.list_container}}">
<table class="w-full text-sm border table-fixed">
<thead class="bg-stone-100">
<tr>
<th class="p-2 text-left w-1/6">Name</th>
<th class="p-2 text-left w-1/6">Flexible</th>
<th class="text-left p-2 w-1/6">Days</th>
<th class="text-left p-1/6">Time</th>
<th class="text-left p-2 w-1/6">Cost</th>
<th class="text-left p-2 w-1/6">Actions</th>
</tr>
</thead>
<tbody>
{% for s in slots %}
{% include '_types/slots/_row.html' %}
{% else %}
<tr><td colspan="5" class="p-3 text-stone-500">No slots yet.</td></tr>
{% endfor %}
</tbody>
</table>
<!-- This is what HTMX will swap between button and form -->
<div id="slot-add-container" class="mt-4">
{% include '_types/slots/_add_button.html' %}
</div>
</section>

View File

@@ -1,15 +0,0 @@
{% extends "oob_elements.html" %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header('calendar-header-child', 'slots-header-child', '_types/slots/header/_header.html')}}
{% from '_types/calendar/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block content %}
{% include '_types/slots/_main_panel.html' %}
{% endblock %}

View File

@@ -1,61 +0,0 @@
{% import 'macros/links.html' as links %}
<tr class="{{ styles.tr }}">
<td class="p-2 align-top w-1/6">
<div class="font-medium">
{% call links.link(
hx_select_search,
aclass=styles.pill
) %}
{{ s.name }}
{% endcall %}
</div>
{% set slot = s %}
{% include '_types/slot/_description.html' %}
</td>
<td class="p-2 align-top w-1/6">
{{ 'yes' if s.flexible else 'no' }}
</td>
<td class="p-2 align-top w-1/6">
{% set days = s.days_display.split(', ') %}
{% if days and days[0] != "—" %}
<div class="flex flex-wrap gap-1">
{% for day in days %}
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-200">
{{ day }}
</span>
{% endfor %}
</div>
{% else %}
<span class="text-xs text-slate-400">No days</span>
{% endif %}
</td>
<td class="p-2 align-top w-1/6">
{{ s.time_start.strftime('%H:%M') }} - {{ s.time_end.strftime('%H:%M') }}
</td>
<td class="p-2 align-top w-1/6">
{{ ('%.2f'|format(s.cost)) if s.cost is not none else '' }}
</td>
<td class="p-2 align-top w-1/6">
<button
class="{{styles.action_button}}"
data-confirm="true"
data-confirm-title="Delete slot?"
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"
sx-delete="{{ url_for('calendar.slots.slot.slot_delete',
calendar_slug=calendar.slug,
slot_id=s.id) }}"
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>
</button>
</td>
</tr>

View File

@@ -1,18 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='slots-row', oob=oob) %}
{% call links.link(
hx_select_search,
) %}
<i class="fa fa-clock"></i>
<div class="shrink-0">
slots
</div>
{% endcall %}
{% call links.desktop_nav() %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,19 +0,0 @@
{% extends '_types/calendar/index.html' %}
{% block calendar_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('slots-header-child', '_types/slots/header/_header.html') %}
{% block slots_header_child %}
{% endblock %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{#% include '_types/calendar/_nav.html' %#}
{% endblock %}
{% block content %}
{% include '_types/slots/_main_panel.html' %}
{% endblock %}

View File

@@ -1,39 +0,0 @@
{# Check-in result — replaces ticket row or action area #}
{% if success and ticket %}
<tr class="bg-blue-50" id="ticket-row-{{ ticket.code }}">
<td class="px-4 py-3">
<span class="font-mono text-xs">{{ ticket.code[:12] }}...</span>
</td>
<td class="px-4 py-3">
<div class="font-medium">{{ ticket.entry.name if ticket.entry else '—' }}</div>
{% if ticket.entry and ticket.entry.start_at %}
<div class="text-xs text-stone-500">
{{ ticket.entry.start_at.strftime('%d %b %Y, %H:%M') }}
</div>
{% endif %}
</td>
<td class="px-4 py-3 text-sm">
{{ ticket.ticket_type.name if ticket.ticket_type else '—' }}
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800">
Checked in
</span>
</td>
<td class="px-4 py-3">
<span class="text-xs text-blue-600">
<i class="fa fa-check-circle" aria-hidden="true"></i>
{% if ticket.checked_in_at %}
{{ ticket.checked_in_at.strftime('%H:%M') }}
{% else %}
Just now
{% endif %}
</span>
</td>
</tr>
{% elif not success %}
<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"></i>
{{ error or 'Check-in failed' }}
</div>
{% endif %}

View File

@@ -1,75 +0,0 @@
{# Tickets for a specific calendar entry — admin view #}
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">
Tickets for: {{ entry.name }}
</h3>
<span class="text-sm text-stone-500">
{{ tickets|length }} ticket{{ 's' if tickets|length != 1 else '' }}
</span>
</div>
{% if tickets %}
<div class="overflow-x-auto rounded-xl border border-stone-200">
<table class="w-full text-sm">
<thead class="bg-stone-50">
<tr>
<th class="px-4 py-2 text-left font-medium text-stone-600">Code</th>
<th class="px-4 py-2 text-left font-medium text-stone-600">Type</th>
<th class="px-4 py-2 text-left font-medium text-stone-600">State</th>
<th class="px-4 py-2 text-left font-medium text-stone-600">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-stone-100">
{% for ticket in tickets %}
<tr class="hover:bg-stone-50" id="entry-ticket-row-{{ ticket.code }}">
<td class="px-4 py-2 font-mono text-xs">{{ ticket.code[:12] }}...</td>
<td class="px-4 py-2">{{ ticket.ticket_type.name if ticket.ticket_type else '—' }}</td>
<td class="px-4 py-2">
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
{% if ticket.state == 'confirmed' %}
bg-emerald-100 text-emerald-800
{% elif ticket.state == 'checked_in' %}
bg-blue-100 text-blue-800
{% elif ticket.state == 'reserved' %}
bg-amber-100 text-amber-800
{% else %}
bg-stone-100 text-stone-700
{% endif %}
">
{{ ticket.state|replace('_', ' ')|capitalize }}
</span>
</td>
<td class="px-4 py-2">
{% if ticket.state in ('confirmed', 'reserved') %}
<form
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
type="submit"
class="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
>
Check in
</button>
</form>
{% elif ticket.state == 'checked_in' %}
<span class="text-xs text-blue-600">
<i class="fa fa-check-circle" aria-hidden="true"></i>
{% if ticket.checked_in_at %}{{ ticket.checked_in_at.strftime('%H:%M') }}{% endif %}
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-6 text-stone-500 text-sm">
No tickets for this entry
</div>
{% endif %}
</div>

Some files were not shown because too many files have changed in this diff Show More