diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py
index ae679d186c..14077a7a09 100644
--- a/src/pretix/base/settings.py
+++ b/src/pretix/base/settings.py
@@ -933,6 +933,23 @@ DEFAULTS = {
"is over. You can use it to describe other options to get a ticket, such as a box office.")
)
},
+ 'event_list_empty_text': {
+ 'default': LazyI18nString.from_gettext(
+ gettext_noop('No dates are currently available for sale that match your criteria.'),
+ ),
+ 'type': LazyI18nString,
+ 'form_class': I18nFormField,
+ 'serializer_class': I18nField,
+ 'form_kwargs': dict(
+ label=pgettext_lazy("subevents", "Text for empty date results"),
+ widget=I18nMarkdownTextarea,
+ widget_kwargs={'attrs': {'rows': '2'}},
+ help_text=pgettext_lazy("subevents", "This text will be shown if the calendar or list of dates is empty, "
+ "e.g. because a month does not contain any dates or a filter chosen by the user does "
+ "find any results. You can use this to advertise ways to get in touch with you to "
+ "arrange further dates. We do not recommend more than one or two sentences.")
+ )
+ },
'payment_explanation': {
'default': '',
'type': LazyI18nString,
diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py
index 82087bbe67..39d75bd180 100644
--- a/src/pretix/control/forms/event.py
+++ b/src/pretix/control/forms/event.py
@@ -600,6 +600,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
'imprint_url',
'checkout_email_helptext',
'presale_has_ended_text',
+ 'event_list_empty_text',
'voucher_explanation_text',
'checkout_success_text',
'show_dates_on_frontpage',
@@ -734,6 +735,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_available_only']
del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
+ del self.fields['event_list_empty_text']
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
], key=lambda a: a[0])
diff --git a/src/pretix/control/templates/pretixcontrol/event/settings.html b/src/pretix/control/templates/pretixcontrol/event/settings.html
index 8077e57189..4655427dba 100644
--- a/src/pretix/control/templates/pretixcontrol/event/settings.html
+++ b/src/pretix/control/templates/pretixcontrol/event/settings.html
@@ -275,6 +275,9 @@
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
{% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
+ {% if sform.event_list_empty_text %}
+ {% bootstrap_field sform.event_list_empty_text layout="control" addon_after="%" %}
+ {% endif %}
{% trans "Order details" %}
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html
index 7402b12a54..38eca95caa 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar.html
@@ -2,6 +2,7 @@
{% load eventurl %}
{% load icon %}
{% load urlreplace %}
+{% load rich_text %}
-{% include "pretixpresale/fragment_calendar.html" with show_avail=event.settings.event_list_availability weeks=subevent_list.weeks show_names=subevent_list.show_names %}
+
+ {% include "pretixpresale/fragment_calendar.html" with show_avail=event.settings.event_list_availability weeks=subevent_list.weeks show_names=subevent_list.show_names %}
+ {% if subevent_list.any_events is False %}
+
+
+ {{ event.settings.event_list_empty_text|rich_text }}
+
+ {% endif %}
+
diff --git a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html
index fb53d66e3d..18e849812f 100644
--- a/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html
+++ b/src/pretix/presale/templates/pretixpresale/event/fragment_subevent_calendar_week.html
@@ -2,6 +2,7 @@
{% load eventurl %}
{% load icon %}
{% load urlreplace %}
+{% load rich_text %}
-{% include "pretixpresale/fragment_week_calendar.html" with show_avail=event.settings.event_list_availability days=subevent_list.days show_names=subevent_list.show_names %}
+
+ {% include "pretixpresale/fragment_week_calendar.html" with show_avail=event.settings.event_list_availability days=subevent_list.days show_names=subevent_list.show_names %}
+ {% if subevent_list.any_events is False %}
+
+
+ {{ event.settings.event_list_empty_text|rich_text }}
+
+ {% endif %}
+
{% for subev in subevent_list.subevent_list %}
@@ -43,5 +44,10 @@
+{% empty %}
+
+
+ {{ event.settings.event_list_empty_text|rich_text }}
+
{% endfor %}
diff --git a/src/pretix/presale/views/event.py b/src/pretix/presale/views/event.py
index 4d48902d75..a7cff433eb 100644
--- a/src/pretix/presale/views/event.py
+++ b/src/pretix/presale/views/event.py
@@ -356,6 +356,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
after,
future_only=self.request.event.settings.event_calendar_future_only
)
+ context['any_events'] = any(ebd)
elif context['list_type'] == "week":
self._set_week_year()
tz = self.request.event.timezone
@@ -430,6 +431,7 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
after,
future_only=self.request.event.settings.event_calendar_future_only
)
+ context['any_events'] = any(ebd)
else:
subevents = self.request.event.subevents_sorted(
filter_qs_by_attr(
diff --git a/src/pretix/presale/views/widget.py b/src/pretix/presale/views/widget.py
index e0a28311b5..872a04d829 100644
--- a/src/pretix/presale/views/widget.py
+++ b/src/pretix/presale/views/widget.py
@@ -642,6 +642,8 @@ class WidgetAPIProductList(EventListMixin, View):
sales_channel=self.request.sales_channel,
)
+ if not any(ebd):
+ data['empty_text'] = str(rich_text(request.event.settings.event_list_empty_text, safelinks=False))
data['weeks'] = weeks_for_template(ebd, self.year, self.month)
for w in data['weeks']:
for d in w:
@@ -700,6 +702,8 @@ class WidgetAPIProductList(EventListMixin, View):
sales_channel=self.request.sales_channel,
)
+ if not any(ebd):
+ data['empty_text'] = str(rich_text(request.event.settings.event_list_empty_text, safelinks=False))
data['days'] = days_for_template(ebd, week)
for d in data['days']:
d['events'] = self._serialize_events(d['events'] or [])
@@ -770,6 +774,8 @@ class WidgetAPIProductList(EventListMixin, View):
'event_url': eventreverse_absolute(event, 'presale:event.index'),
})
+ if not data['events']:
+ data['empty_text'] = str(rich_text(request.event.settings.event_list_empty_text, safelinks=False))
cache.set(cache_key, data, 30)
# These pages are cached for a really short duration – this should make them pretty accurate, while still
# providing some protection against burst traffic.
diff --git a/src/pretix/static/pretixpresale/scss/_calendar.scss b/src/pretix/static/pretixpresale/scss/_calendar.scss
index 5ff92a785c..4e1fc9ae72 100644
--- a/src/pretix/static/pretixpresale/scss/_calendar.scss
+++ b/src/pretix/static/pretixpresale/scss/_calendar.scss
@@ -1,3 +1,35 @@
+.calendar-container:has(.calendar-empty), .event-list:has(.calendar-empty) {
+ position: relative;
+ overflow: hidden;
+
+ .table-responsive, .week-calendar {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ z-index: 0;
+ }
+
+ .calendar-empty {
+ position: relative;
+ z-index: 100;
+ background: rgba(255, 255, 255, 0.9);
+ text-align: center;
+ font-size: $font-size-large;
+ padding: 30px 0;
+
+ .fa {
+ margin-block: 12px;
+ }
+
+ *:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+}
.table-calendar, .week-calendar, .day-calendar {
td, th {
width: 14.29%;
diff --git a/src/pretix/static/pretixpresale/scss/main.scss b/src/pretix/static/pretixpresale/scss/main.scss
index d598c8d928..8b2e2a5058 100644
--- a/src/pretix/static/pretixpresale/scss/main.scss
+++ b/src/pretix/static/pretixpresale/scss/main.scss
@@ -513,7 +513,7 @@ h2 .label {
}
.event-list.full-width-list {
- h3, h4, p {
+ h3, h4, article p {
margin-top: 0;
margin-bottom: 0;
}
diff --git a/src/pretix/static/pretixpresale/scss/widget.scss b/src/pretix/static/pretixpresale/scss/widget.scss
index 9060ea0469..5c9905c341 100644
--- a/src/pretix/static/pretixpresale/scss/widget.scss
+++ b/src/pretix/static/pretixpresale/scss/widget.scss
@@ -748,6 +748,13 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
display: block;
width: 100%;
}
+
+ .pretix-widget-event-list-empty,
+ .pretix-widget-event-week-empty,
+ .pretix-widget-event-calendar-empty {
+ padding: 10px 0;
+ text-align: center;
+ }
}
diff --git a/src/pretix/static/pretixpresale/widget/src/api.ts b/src/pretix/static/pretixpresale/widget/src/api.ts
index cbe0a9e19a..a43b8acf99 100644
--- a/src/pretix/static/pretixpresale/widget/src/api.ts
+++ b/src/pretix/static/pretixpresale/widget/src/api.ts
@@ -38,6 +38,7 @@ export interface ProductListResponse {
events?: EventEntry[]
has_more_events?: boolean
meta_filter_fields?: MetaFilterField[]
+ empty_text?: string
weeks?: DayEntry[][]
date?: string
days?: DayEntry[]
diff --git a/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue b/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue
index b1ed41aa95..8971e85c4f 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/EventCalendar.vue
@@ -92,7 +92,9 @@ function nextmonth () {
| {{ STRINGS.next_month }} »
//- Calendar
+ .pretix-widget-event-calendar-empty(v-if="!!store.emptyText", v-html="store.emptyText")
table.pretix-widget-event-calendar-table(
+ v-else
:id="id",
tabindex="0",
:aria-labelledby="ariaLabelledby"
diff --git a/src/pretix/static/pretixpresale/widget/src/components/EventList.vue b/src/pretix/static/pretixpresale/widget/src/components/EventList.vue
index b874675beb..198619d8e8 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/EventList.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/EventList.vue
@@ -62,6 +62,8 @@ console.log(store)
:event="event"
)
+ .pretix-widget-event-list-empty(v-if="!store.events.length && store.emptyText", v-html="store.emptyText")
+
p.pretix-widget-event-list-load-more(v-if="store.hasMoreEvents")
button(@click.prevent.stop="loadMore") {{ STRINGS.load_more }}
diff --git a/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue b/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue
index ad0a2cdeb8..63996693d3 100644
--- a/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue
+++ b/src/pretix/static/pretixpresale/widget/src/components/EventWeekCalendar.vue
@@ -88,7 +88,8 @@ function nextweek () {
| {{ STRINGS.next_week }} »
//- Actual calendar
- .pretix-widget-event-week-table(:id="id", tabindex="0", :aria-label="weekname")
+ .pretix-widget-event-week-empty(v-if="store.emptyText", v-html="store.emptyText")
+ .pretix-widget-event-week-table(v-else, :id="id", tabindex="0", :aria-label="weekname")
.pretix-widget-event-week-col(v-for="d in store.days", :key="d?.date || ''")
EventWeekCell(:day="d", :mobile="mobile")
diff --git a/src/pretix/static/pretixpresale/widget/src/sharedStore.ts b/src/pretix/static/pretixpresale/widget/src/sharedStore.ts
index 00d36c02f7..c2273e299b 100644
--- a/src/pretix/static/pretixpresale/widget/src/sharedStore.ts
+++ b/src/pretix/static/pretixpresale/widget/src/sharedStore.ts
@@ -95,6 +95,7 @@ export function createWidgetStore (config: {
offset: 0,
appendEvents: false,
metaFilterFields: [] as MetaFilterField[],
+ emptyText: null as string | null,
// UI state
mobile: false,
@@ -270,6 +271,7 @@ export function createWidgetStore (config: {
this.name = data.name ?? null
this.frontpageText = data.frontpage_text ?? null
this.metaFilterFields = data.meta_filter_fields ?? []
+ this.emptyText = data.empty_text ?? null
} else if (data.days !== undefined) {
this.days = data.days
this.date = null
@@ -279,6 +281,7 @@ export function createWidgetStore (config: {
this.name = data.name ?? null
this.frontpageText = data.frontpage_text ?? null
this.metaFilterFields = data.meta_filter_fields ?? []
+ this.emptyText = data.empty_text ?? null
} else if (data.events !== undefined) {
this.events = this.appendEvents && this.events
? this.events.concat(data.events)
@@ -290,6 +293,7 @@ export function createWidgetStore (config: {
this.frontpageText = data.frontpage_text ?? null
this.hasMoreEvents = data.has_more_events ?? false
this.metaFilterFields = data.meta_filter_fields ?? []
+ this.emptyText = data.empty_text ?? null
} else {
this.view = 'event'
this.targetUrl = data.target_url ?? this.targetUrl