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