Remove dead code: domain_event.py + 39 overridden templates

- Delete shared/models/domain_event.py (table dropped, model orphaned)
- Delete 39 shared templates that are overridden by app-local copies:
  - 8 blog overrides (blog/_action_buttons, post/_meta, etc.)
  - 27 events overrides (calendar/*, day/*, entry/*, post_entries/*)
  - 4 market overrides (market/index, browse/_oob_elements, etc.)

These shared copies were never served — Quart loads app-level
templates first, so the app-local versions always win.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-22 18:09:02 +00:00
parent 7de4a2e40e
commit 46f6ca4a0f
41 changed files with 1 additions and 2554 deletions

View File

@@ -1,51 +0,0 @@
{# New Post + Drafts toggle — shown in aside (desktop + mobile) #}
<div class="flex flex-wrap gap-2 px-4 py-3">
{% if has_access('blog.new_post') %}
{% set new_href = url_for('blog.new_post')|host %}
<a
href="{{ new_href }}"
hx-get="{{ new_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
class="px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors"
title="New Post"
>
<i class="fa fa-plus mr-1"></i> New Post
</a>
{% endif %}
{% if g.user and (draft_count or drafts) %}
{% if drafts %}
{% set drafts_off_href = (current_local_href ~ {'drafts': None}|qs)|host %}
<a
href="{{ drafts_off_href }}"
hx-get="{{ drafts_off_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
class="px-3 py-1 rounded bg-stone-700 text-white text-sm hover:bg-stone-800 transition-colors"
title="Hide Drafts"
>
<i class="fa fa-file-text-o mr-1"></i> Drafts
<span class="inline-block bg-stone-500 text-white px-1.5 py-0.5 text-xs font-medium rounded ml-1">{{ draft_count }}</span>
</a>
{% else %}
{% set drafts_on_href = (current_local_href ~ {'drafts': '1'}|qs)|host %}
<a
href="{{ drafts_on_href }}"
hx-get="{{ drafts_on_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
class="px-3 py-1 rounded bg-amber-600 text-white text-sm hover:bg-amber-700 transition-colors"
title="Show Drafts"
>
<i class="fa fa-file-text-o mr-1"></i> Drafts
<span class="inline-block bg-amber-500 text-white px-1.5 py-0.5 text-xs font-medium rounded ml-1">{{ draft_count }}</span>
</a>
{% endif %}
{% endif %}
</div>

View File

@@ -1,48 +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 }}"
hx-get="{{ list_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' }}"
title="List view"
_="on click js localStorage.removeItem('blog_view') end"
>
<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 }}"
hx-get="{{ tile_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' }}"
title="Tile view"
_="on click js localStorage.setItem('blog_view','tile') end"
>
<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 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/blog/_cards.html" %}
</div>
{% else %}
<div class="max-w-full px-3 py-3 space-y-3">
{% include "_types/blog/_cards.html" %}
</div>
{% endif %}
<div class="pb-8"></div>

View File

@@ -1,37 +0,0 @@
{% extends 'oob_elements.html' %}
{# OOB elements for HTMX navigation - all elements that need updating #}
{# 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 %}
{# 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('root-header-child', 'market-header-child', '_types/market/header/_header.html')}}
{% from '_types/root/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/market/mobile/_nav_panel.html' %}
{% endblock %}
{# Filter container with child summary - from browse/index.html child_summary block #}
{% block filter %}
{% include "_types/browse/mobile/_filter/summary.html" %}
{% endblock %}
{% block aside %}
{% include "_types/browse/desktop/menu.html" %}
{% endblock %}
{% block content %}
{% include "_types/browse/_main_panel.html" %}
{% endblock %}

View File

@@ -1,180 +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('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=prev_year,
month=month) }}"
hx-get="{{ url_for('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=prev_year,
month=month) }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-swap="outerHTML"
hx-push-url="true"
>
&laquo;
</a>
{# Inner left: -1 month #}
<a
class="{{styles.pill}} text-xl"
href="{{ url_for('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=prev_month_year,
month=prev_month) }}"
hx-get="{{ url_for('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=prev_month_year,
month=prev_month) }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-swap="outerHTML"
hx-push-url="true"
>
&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('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=next_month_year,
month=next_month) }}"
hx-get="{{ url_for('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=next_month_year,
month=next_month) }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-swap="outerHTML"
hx-push-url="true"
>
&rsaquo;
</a>
{# Outer right: +1 year #}
<a
class="{{styles.pill}} text-xl"
href="{{ url_for('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=next_year,
month=month) }}"
hx-get="{{ url_for('calendars.calendar.get',
slug=post.slug,
calendar_slug=calendar.slug,
year=next_year,
month=month) }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-swap="outerHTML"
hx-push-url="true"
>
&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('calendars.calendar.day.show_day',
slug=post.slug,
calendar_slug=calendar.slug,
year=day.date.year,
month=day.date.month,
day=day.date.day) }}"
hx-get="{{ url_for('calendars.calendar.day.show_day',
slug=post.slug,
calendar_slug=calendar.slug,
year=day.date.year,
month=day.date.month,
day=day.date.day) }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-swap="outerHTML"
hx-push-url="true"
>
{{ 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,33 +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"
hx-get="{{ url_for(
'calendars.calendar.admin.calendar_description_edit',
slug=post.slug,
calendar_slug=calendar.slug,
) }}"
hx-target="#calendar-description"
hx-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,43 +0,0 @@
<div id="calendar-description">
<form
hx-post="{{ url_for(
'calendars.calendar.admin.calendar_description_save',
slug=post.slug,
calendar_slug=calendar.slug,
) }}"
hx-target="#calendar-description"
hx-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"
hx-get="{{ url_for(
'calendars.calendar.admin.calendar_description_view',
slug=post.slug,
calendar_slug=calendar.slug,
) }}"
hx-target="#calendar-description"
hx-swap="outerHTML"
>
Cancel
</button>
</div>
</form>
</div>

View File

@@ -1,46 +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"
hx-put="{{ url_for('calendars.calendar.put', slug=post.slug, calendar_slug=calendar.slug ) }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-on::before-request="document.querySelector('#cal-put-errors').textContent='';"
hx-on::response-error="document.querySelector('#cal-put-errors').innerHTML = event.detail.xhr.responseText;"
hx-on::after-request="if (event.detail.successful) this.reset()"
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,14 +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(
url_for('calendars.calendar.admin.admin', slug=post.slug, calendar_slug=calendar.slug),
hx_select_search
) %}
{{ links.admin() }}
{% endcall %}
{% call links.desktop_nav() %}
{% include '_types/calendar/admin/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

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) %}
<a href="{{ events_url('/' + post.slug + '/calendars/' + calendar.slug + '/') }}" class="flex items-center gap-2 px-3 py-2 rounded whitespace-normal text-center break-words leading-snug">
<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>
</a>
{% call links.desktop_nav() %}
{% include '_types/calendar/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

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

View File

@@ -1,301 +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"
hx-post="{{ url_for(
'calendars.calendar.day.calendar_entries.add_entry',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#day-entries"
hx-on::after-request="if (event.detail.successful) this.reset()"
hx-swap="innerHTML"
>
<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}}"
hx-get="{{ url_for('calendars.calendar.day.calendar_entries.add_button',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#entry-add-container"
hx-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,17 +0,0 @@
<button
type="button"
class="{{styles.pre_action_button}}"
hx-get="{{ url_for(
'calendars.calendar.day.calendar_entries.add_form',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#entry-add-container"
hx-swap="innerHTML"
>
+ Add entry
</button>

View File

@@ -1,50 +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('calendars.calendar.day.calendar_entries.calendar_entry.get',
slug=post.slug,
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>
{# Container nav widgets (market links, etc.) #}
{% if container_nav_widgets %}
{% for wdata in container_nav_widgets %}
{% with ctx=wdata.ctx %}
{% include wdata.widget.template with context %}
{% endwith %}
{% endfor %}
{% endif %}
{# Admin link #}
{% if g.rights.admin %}
{% from 'macros/admin_nav.html' import admin_nav_item %}
{{admin_nav_item(
url_for(
'calendars.calendar.day.admin.admin',
slug=post.slug,
calendar_slug=calendar.slug,
year=day_date.year,
month=day_date.month,
day=day_date.day
)
)}}
{% endif %}

View File

@@ -1,76 +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(
'calendars.calendar.day.calendar_entries.calendar_entry.get',
slug=post.slug,
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(
'calendars.calendar.slots.slot.get',
slug=post.slug,
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,34 +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"
hx-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('calendars.calendar.day.calendar_entries.calendar_entry.get',
slug=post.slug,
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" hx-swap-oob="true"></div>
{% endif %}

View File

@@ -1,21 +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(
'calendars.calendar.day.admin.admin',
slug=post.slug,
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,27 +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(
'calendars.calendar.day.show_day',
slug=post.slug,
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,334 +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"
hx-put="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.put',
slug=post.slug,
calendar_slug=calendar.slug,
day=day, month=month, year=year,
entry_id=entry.id
) }}"
hx-target="#entry-{{ entry.id }}"
hx-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 }}"
hx-get="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.get',
slug=post.slug,
calendar_slug=calendar.slug,
day=day, month=month, year=year,
entry_id=entry.id
) }}"
hx-target="#entry-{{ entry.id }}"
hx-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,126 +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>
<!-- 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}}"
hx-get="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.get_edit',
entry_id=entry.id,
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#entry-{{entry.id}}"
hx-swap="outerHTML"
>
Edit
</button>
</div>
</section>

View File

@@ -1,48 +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="{{ coop_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>
{% if container_nav_widgets %}
{% for wdata in container_nav_widgets %}
{% with ctx=wdata.ctx %}
{% include wdata.widget.template with context %}
{% endwith %}
{% endfor %}
{% endif %}
{# Admin link #}
{% if g.rights.admin %}
{% from 'macros/admin_nav.html' import admin_nav_item %}
{{admin_nav_item(
url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.admin.admin',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
)
)}}
{% endif %}

View File

@@ -1,98 +0,0 @@
<div id="calendar_entry_options_{{ entry.id }}" class="flex flex-col md:flex-row gap-1">
{% if entry.state == 'provisional' %}
<form
hx-post="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.confirm_entry',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
hx-select="#calendar_entry_options_{{ entry.id }}"
hx-target="#calendar_entry_options_{{entry.id}}"
hx-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
hx-post="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.decline_entry',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
hx-select="#calendar_entry_options_{{ entry.id }}"
hx-target="#calendar_entry_options_{{entry.id}}"
hx-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
hx-post="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.provisional_entry',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
hx-target="#calendar_entry_options_{{ entry.id }}"
hx-select="#calendar_entry_options_{{ entry.id }}"
hx-swap="outerHTML"
hx-trigger="confirmed"
>
<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,74 +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"
hx-delete="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.remove_post',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id,
post_id=entry_post.id
) }}"
hx-trigger="confirmed"
hx-target="#entry-posts-{{entry.id}}"
hx-swap="innerHTML"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
>
<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"
hx-get="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.search_posts',
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
entry_id=entry.id
) }}"
hx-trigger="keyup changed delay:300ms, load"
hx-target="#post-search-results-{{entry.id}}"
hx-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,105 +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"
hx-post="{{ url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.update_tickets',
entry_id=entry.id,
slug=post.slug,
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#entry-tickets-{{entry.id}}"
hx-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,18 +0,0 @@
{% import 'macros/links.html' as links %}
{% call links.link(
url_for(
'calendars.calendar.day.calendar_entries.calendar_entry.ticket_types.get',
slug=post.slug,
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,22 +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(
'calendars.calendar.day.calendar_entries.calendar_entry.admin.admin',
slug=post.slug,
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,28 +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(
'calendars.calendar.day.calendar_entries.calendar_entry.get',
slug=post.slug,
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,30 +0,0 @@
{% extends 'oob_elements.html' %}
{# OOB elements for HTMX navigation - all elements that need updating #}
{# 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 %}
{# 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('root-header-child', 'market-header-child', '_types/market/header/_header.html')}}
{% from '_types/root/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/market/mobile/_nav_panel.html' %}
{% endblock %}
{% block content %}
{% include "_types/market/_main_panel.html" %}
{% endblock %}

View File

@@ -1,25 +0,0 @@
{% extends '_types/root/_index.html' %}
{% block root_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('market-header-child', '_types/market/header/_header.html') %}
{% block market_header_child %}
{% endblock %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/market/mobile/_nav_panel.html' %}
{% endblock %}
{% block aside %}
{# No aside on landing page #}
{% endblock %}
{% block content %}
{% include "_types/market/_main_panel.html" %}
{% endblock %}

View File

@@ -1,128 +0,0 @@
{# --- social/meta_post.html --- #}
{# Context expected:
site, post, request
#}
{% if post is not defined %}
{% include 'social/meta_base.html' %}
{% else %}
{# Visibility → robots #}
{% set is_public = (post.visibility == 'public') %}
{% set is_published = (post.status == 'published') %}
{% set robots_here = 'index,follow' if (is_public and is_published and not post.email_only) else 'noindex,nofollow' %}
{# Compute canonical early so both this file and base can use it #}
{% set _site_url = site().url.rstrip('/') if site and site().url else '' %}
{% set _post_path = request.path if request else ('/posts/' ~ (post.slug or post.uuid)) %}
{% set canonical = post.canonical_url or (_site_url ~ _post_path if _site_url else (request.url if request else None)) %}
{# Include common base (charset, viewport, robots default, RSS, Org/WebSite JSON-LD) #}
{% set robots_override = robots_here %}
{% include 'social/meta_base.html' %}
{# ---- Titles / descriptions ---- #}
{% set og_title = post.og_title or base_title %}
{% set tw_title = post.twitter_title or base_title %}
{# Description best-effort, trimmed #}
{% set desc_source = post.meta_description
or post.og_description
or post.twitter_description
or post.custom_excerpt
or post.excerpt
or (post.plaintext if post.plaintext else (post.html|striptags if post.html else '')) %}
{% set description = (desc_source|trim|replace('\n',' ')|replace('\r',' ')|striptags)|truncate(160, True, '…') %}
{# Image priority #}
{% set image_url = post.og_image
or post.twitter_image
or post.feature_image
or (site().default_image if site and site().default_image else None) %}
{# Dates #}
{% set published_iso = post.published_at.isoformat() if post.published_at else None %}
{% set updated_iso = post.updated_at.isoformat() if post.updated_at
else (post.created_at.isoformat() if post.created_at else None) %}
{# Authors / tags #}
{% set primary_author = post.primary_author %}
{% set authors = post.authors or ([primary_author] if primary_author else []) %}
{% set tag_names = (post.tags or []) | map(attribute='name') | list %}
{% set is_article = not post.is_page %}
<title>{{ base_title }}</title>
<meta name="description" content="{{ description }}">
{% if canonical %}<link rel="canonical" href="{{ canonical }}">{% endif %}
{# ---- Open Graph ---- #}
<meta property="og:site_name" content="{{ site().title if site and site().title else '' }}">
<meta property="og:type" content="{{ 'article' if is_article else 'website' }}">
<meta property="og:title" content="{{ og_title }}">
<meta property="og:description" content="{{ description }}">
{% if canonical %}<meta property="og:url" content="{{ canonical }}">{% endif %}
{% if image_url %}<meta property="og:image" content="{{ image_url }}">{% endif %}
{% if is_article and published_iso %}<meta property="article:published_time" content="{{ published_iso }}">{% endif %}
{% if is_article and updated_iso %}
<meta property="article:modified_time" content="{{ updated_iso }}">
<meta property="og:updatd_time" content="{{ updated_iso }}">
{% endif %}
{% if is_article and post.primary_tag and post.primary_tag.name %}
<meta property="article:section" content="{{ post.primary_tag.name }}">
{% endif %}
{% if is_article %}
{% for t in tag_names %}
<meta property="article:tag" content="{{ t }}">
{% endfor %}
{% endif %}
{# ---- Twitter ---- #}
<meta name="twitter:card" content="{{ 'summary_large_image' if image_url else 'summary' }}">
{% if site and site().twitter_site %}<meta name="twitter:site" content="{{ site().twitter_site }}">{% endif %}
{% if primary_author and primary_author.twitter %}
<meta name="twitter:creator" content="@{{ primary_author.twitter | replace('@','') }}">
{% endif %}
<meta name="twitter:title" content="{{ tw_title }}">
<meta name="twitter:description" content="{{ description }}">
{% if image_url %}<meta name="twitter:image" content="{{ image_url }}">{% endif %}
{# ---- JSON-LD author value (no list comprehensions) ---- #}
{% if authors and authors|length == 1 %}
{% set author_value = {"@type": "Person", "name": authors[0].name} %}
{% elif authors %}
{% set ns = namespace(arr=[]) %}
{% for a in authors %}
{% set _ = ns.arr.append({"@type": "Person", "name": a.name}) %}
{% endfor %}
{% set author_value = ns.arr %}
{% else %}
{% set author_value = none %}
{% endif %}
{# ---- JSON-LD using combine for optionals ---- #}
{% set jsonld = {
"@context": "https://schema.org",
"@type": "BlogPosting" if is_article else "WebPage",
"mainEntityOfPage": canonical,
"headline": base_title,
"description": description,
"image": image_url,
"datePublished": published_iso,
"author": author_value,
"publisher": {
"@type": "Organization",
"name": site().title if site and site().title else "",
"logo": {"@type": "ImageObject", "url": site().logo if site and site().logo else image_url}
}
} %}
{% if updated_iso %}
{% set jsonld = jsonld | combine({"dateModified": updated_iso}) %}
{% endif %}
{% if tag_names %}
{% set jsonld = jsonld | combine({"keywords": tag_names | join(", ")}) %}
{% endif %}
<script type="application/ld+json">
{{ jsonld | tojson }}
</script>
{% endif %}

View File

@@ -1,8 +0,0 @@
{% import 'macros/links.html' as links %}
{# Widget-driven container nav — entries, calendars, markets #}
{% if container_nav_widgets %}
<div class="flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
id="entries-calendars-nav-wrapper">
{% include '_types/post/admin/_nav_entries.html' %}
</div>
{% endif %}

View File

@@ -1,18 +0,0 @@
{% import 'macros/links.html' as links %}
<div class="relative nav-group">
<a href="{{ events_url('/' + post.slug + '/calendars/') }}" class="{{styles.nav_button}}">
calendars
</a>
</div>
{% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
entries
{% endcall %}
{% call links.link(url_for('blog.post.admin.data', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
data
{% endcall %}
{% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
edit
{% endcall %}
{% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
settings
{% endcall %}

View File

@@ -1,22 +0,0 @@
{% extends "oob_elements.html" %}
{# OOB elements for post 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('post-header-child', 'post-admin-header-child', '_types/post/admin/header/_header.html')}}
{% from '_types/post/header/_header.html' import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{% block mobile_menu %}
{% include '_types/post/admin/_nav.html' %}
{% endblock %}
{% block content %}
nowt
{% endblock %}

View File

@@ -1,18 +0,0 @@
{% extends '_types/post/index.html' %}
{% import 'macros/layout.html' as layout %}
{% block post_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('post-admin-header-child', '_types/post/admin/header/_header.html') %}
{% block post_admin_header_child %}
{% endblock %}
{% endcall %}
{% endblock %}
{% block _main_mobile_menu %}
{% include '_types/post/admin/_nav.html' %}
{% endblock %}
{% block content %}
nowt
{% endblock %}

View File

@@ -1,19 +0,0 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='post-row', oob=oob) %}
<a href="{{ coop_url('/' + post.slug + '/') }}" class="flex items-center gap-2 px-3 py-2 rounded whitespace-normal text-center break-words leading-snug">
{% 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>
</a>
{% call links.desktop_nav() %}
{% include '_types/post/_nav.html' %}
{% endcall %}
{% endcall %}
{% endmacro %}

View File

@@ -1,48 +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"
_="on toggle
if my.open
for other in <details[open]/>
if other is not me
set other.open to false
end
end
end">
<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"
hx-get="{{ url_for('blog.post.admin.calendar_view', slug=post.slug, calendar_id=calendar.id) }}"
hx-trigger="intersect once"
hx-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,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(url_for('blog.post.admin.entries', slug=post.slug), 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,278 +0,0 @@
{% macro add(slug, cart, oob='false') %}
{% set quantity = cart
| selectattr('product.slug', 'equalto', slug)
| sum(attribute='quantity') %}
<div id="cart-{{ slug }}" {% if oob=='true' %} hx-swap-oob="{{oob}}" {% endif %}>
{% if not quantity %}
<form
action="{{ market_product_url(slug, 'cart') }}"
method="post"
hx-post="{{ market_product_url(slug, 'cart') }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
class="rounded flex items-center"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input
type="hidden"
name="count"
value="1"
>
<button
type="submit"
class="relative inline-flex items-center justify-center text-sm font-medium text-stone-500 hover:bg-emerald-50"
>
<span class="relative inline-flex items-center justify-center">
<i class="fa fa-cart-plus text-4xl" aria-hidden="true"></i>
<!-- black + overlaid in the center -->
</span>
</button>
</form>
{% else %}
<div class="rounded flex items-center gap-2">
<!-- minus -->
<form
action="{{ market_product_url(slug, 'cart') }}"
method="post"
hx-post="{{ market_product_url(slug, 'cart') }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input
type="hidden"
name="count"
value="{{ quantity - 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>
<!-- basket with quantity badge -->
<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-2xl" 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"
>
{{ quantity }}
</span>
</span>
</span>
</a>
<!-- plus -->
<form
action="{{ market_product_url(slug, 'cart') }}"
method="post"
hx-post="{{ market_product_url(slug, 'cart') }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input
type="hidden"
name="count"
value="{{ quantity + 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>
</div>
{% endif %}
</div>
{% endmacro %}
{% macro cart_item(oob=False) %}
{% set p = item.product %}
{% set unit_price = p.special_price or p.regular_price %}
<article
id="cart-item-{{p.slug}}"
{% if oob %}
hx-swap-oob="{{oob}}"
{% endif %}
class="flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5"
>
<div class="w-full sm:w-32 shrink-0 flex justify-center sm:block">
{% if p.image %}
<img
src="{{ p.image }}"
alt="{{ p.title }}"
class="w-24 h-24 sm:w-32 sm:h-28 object-cover rounded-xl border border-stone-100"
loading="lazy"
>
{% else %}
<div
class="w-24 h-24 sm:w-32 sm:h-28 rounded-xl border border-dashed border-stone-300 flex items-center justify-center text-xs text-stone-400"
>
No image
</div>'market', 'product', p.slug
{% endif %}
</div>
{# Details #}
<div class="flex-1 min-w-0">
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3">
<div class="min-w-0">
<h2 class="text-sm sm:text-base md:text-lg font-semibold text-stone-900">
{% set href=market_product_url(p.slug, market_place=item.market_place) %}
<a
href="{{ href }}"
hx_get="{{href}}"
hx-target="#main-panel"
hx-select ="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
class="hover:text-emerald-700"
>
{{ p.title }}
</a>
</h2>
{% if p.brand %}
<p class="mt-0.5 text-[0.7rem] sm:text-xs text-stone-500">
{{ p.brand }}
</p>
{% endif %}
{% if item.is_deleted %}
<p class="mt-2 inline-flex items-center gap-1 text-[0.65rem] sm:text-xs font-medium text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5">
<i class="fa-solid fa-triangle-exclamation text-[0.6rem]" aria-hidden="true"></i>
This item is no longer available or price has changed
</p>
{% endif %}
</div>
{# Unit price #}
<div class="text-left sm:text-right">
{% if unit_price %}
{% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %}
<p class="text-sm sm:text-base font-semibold text-stone-900">
{{ symbol }}{{ "%.2f"|format(unit_price) }}
</p>
{% if p.special_price and p.special_price != p.regular_price %}
<p class="text-xs text-stone-400 line-through">
{{ symbol }}{{ "%.2f"|format(p.regular_price) }}
</p>
{% endif %}
{% else %}
<p class="text-xs text-stone-500">No price</p>
{% endif %}
</div>
</div>
<div class="mt-3 flex flex-col sm:flex-row sm:items-center justify-between gap-2 sm:gap-4">
<div class="flex items-center gap-2 text-xs sm:text-sm text-stone-700">
<span class="text-[0.65rem] sm:text-xs uppercase tracking-wide text-stone-500">Quantity</span>
{% set qty_url = cart_quantity_url(item.product_id) if cart_quantity_url is defined else market_product_url(p.slug, 'cart', item.market_place) %}
<form
action="{{ qty_url }}"
method="post"
hx-post="{{ qty_url }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input
type="hidden"
name="count"
value="{{ [item.quantity - 1, 0] | max }}"
>
<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>
<span class="inline-flex items-center justify-center px-2 py-1 rounded-full bg-stone-100 text-[0.7rem] sm:text-xs font-medium {{ 'text-stone-400' if item.quantity == 0 }}">
{{ item.quantity }}
</span>
<form
action="{{ qty_url }}"
method="post"
hx-post="{{ qty_url }}"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input
type="hidden"
name="count"
value="{{ item.quantity + 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>
{% if cart_delete_url is defined %}
<form
action="{{ cart_delete_url(item.product_id) }}"
method="post"
hx-post="{{ cart_delete_url(item.product_id) }}"
hx-trigger="confirmed"
hx-target="#cart-mini"
hx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button
type="button"
data-confirm
data-confirm-title="Remove item?"
data-confirm-text="Remove {{ p.title }} from your cart?"
data-confirm-icon="warning"
data-confirm-confirm-text="Yes, remove"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
class="inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-red-300 text-red-600 hover:bg-red-50"
title="Remove from cart"
>
<i class="fa-solid fa-trash-can text-xs" aria-hidden="true"></i>
</button>
</form>
{% endif %}
</div>
<div class="flex items-center justify-between sm:justify-end gap-3">
{% if unit_price %}
{% set line_total = unit_price * item.quantity %}
{% set symbol = "£" if p.regular_price_currency == "GBP" else p.regular_price_currency %}
<p class="text-sm sm:text-base font-semibold text-stone-900">
Line total:
{{ symbol }}{{ "%.2f"|format(line_total) }}
</p>
{% endif %}
</div>
</div>
</div>
</article>
{% endmacro %}