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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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"
|
||||
>
|
||||
«
|
||||
</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"
|
||||
>
|
||||
‹
|
||||
</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"
|
||||
>
|
||||
›
|
||||
</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"
|
||||
>
|
||||
»
|
||||
</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>
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user