Demonstrate Upcoming Events Page.
View this pattern on the live demo site
Upcoming events page wrapping the events list section.
{% comment %}
Page layout for listing upcoming events.
{% endcomment -%}
{%- include "/_header.liquid" -%}
{%- include "/sections/_generic_banner.liquid" -%}
{%- include "/sections/_breadcrumbs.liquid" -%}
{%- include "/sections/_upcoming_events.liquid" -%}
{%- include "/sections/_simple_cta.liquid" -%}
{%- include "/_footer.liquid" -%}
Lists upcoming events from the events feed/datastore.
{% comment %}
Display upcoming events, optionally filtered by one or more calendars.
Note that this template uses midnight as the start_date so that the page can be fast-cached
You may switch to start_date:request.date for more accurate results but wil prevent fast-caching
Inputs:
section_class
section_id
Custom Fields
show_upcoming_events
upcoming_events_title
upcoming_events_subtitle
upcoming_events_calendars
upcoming_events_count
upcoming_events_view_all_url
upcoming_events_view_all_label
Outputs:
section_rendered - true when this section outputs markup, false otherwise
{% endcomment -%}
{%- set section_rendered = false -%}
{%- if entity.show_upcoming_events.on -%}
{%- capture var eventfilters -%}
{% if entity.upcoming_events_calendars.is_valid %}calendar:entity.upcoming_events_calendars{% endif %}
start_date:'{{request.month}}/{{request.day}}/{{request.year}}'
sort_by:'start_date'
sort_direction:'asc'
limit:{{entity.upcoming_events_count | default: 3 }}
{%- endcapture -%}
{%- calendar_entries events = *eventfilters -%}
{%- if events.is_valid -%}
{%- include "/partials/_event_detail_modal.liquid" -%}
<section{% if section_class %} class="{{ section_class }}"{% endif %}{% if section_id %} id="{{ section_id }}"{% endif %}>
<div class="container">
{%- var headinglevel = headinglevel %}{% comment %}Copy to current scope{% endcomment -%}
{%- include "/common/_title_with_subtitle.liquid" title:entity.upcoming_events_title subtitle:entity.upcoming_events_subtitle postincrement_headinglevel:true -%}
{%- include "/partials/_upcoming_events.liquid" -%}
{%- if entity.upcoming_events_view_all_url.is_valid -%}
<div class="text-center mt-4">
<a href="{{ entity.upcoming_events_view_all_url.value }}" class="btn btn-outline-primary fw-semibold text-uppercase">{{ entity.upcoming_events_view_all_label | default: 'View All Events' }}</a>
</div>
{%- endif -%}
</div>
</section>
{%- set section_rendered = true -%}
{%- endif -%}
{%- endif -%}
Custom fields owned by the upcoming events section.
[
{
"Type": "toggle",
"FieldID": "show_upcoming_events",
"Label": "Show Upcoming Events",
"DefaultValue": true,
"OnValue": "show",
"OnColor": "green",
"OnLabel": "Show",
"OffValue": "hide",
"OffColor": "red",
"OffLabel": "Hide"
},
{
"Type": "group",
"Condition": {
"FieldID": "show_upcoming_events",
"Type": "eq",
"Value": "show"
},
"Fields": [
{
"Type": "text",
"Label": "Upcoming Events Title",
"FieldID": "upcoming_events_title",
"DefaultValue": "Upcoming Events"
},
{
"Type": "text",
"Label": "Upcoming Events Subtitle",
"FieldID": "upcoming_events_subtitle"
},
{
"Type": "calendarlist",
"FieldID": "upcoming_events_calendars",
"Label": "Calendars",
"Subtext": "Select one or more calendars to filter the events by. Leave blank to include all events."
},
{
"Type": "text",
"FieldID": "upcoming_events_count",
"Label": "Number of Events to Show",
"DefaultValue": 3,
"Validators": [
{
"Type": "number"
},
{
"Type": "min",
"Value": 1
},
{
"Type": "max",
"Value": 100
}
]
},
{
"Type": "url",
"FieldID": "upcoming_events_view_all_url",
"Label": "View All Button URL",
"Subtext": "Optional. Leave blank to hide the button (recommended on the full events listing page)."
},
{
"Type": "text",
"FieldID": "upcoming_events_view_all_label",
"Label": "View All Button Label",
"DefaultValue": "View All Events",
"Condition": {
"Type": "exists",
"FieldID": "upcoming_events_view_all_url"
}
}
],
"Label": "Upcoming Events"
}
]
Shared event-detail modal opened from a list row.
{% comment %}
Shared Bootstrap modal for calendar event details.
Inputs:
modal_id - optional DOM id. Defaults to mpEventDetailModal
{% endcomment -%}
{%- var modal_id = modal_id | default: 'mpEventDetailModal' -%}
<div class="modal fade" id="{{ modal_id }}" tabindex="-1" aria-labelledby="{{ modal_id }}_title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content border-0 shadow">
<div class="modal-header border-0 pb-0">
<div>
<p class="small text-uppercase text-muted mb-1" id="{{ modal_id }}_when"></p>
<h2 class="modal-title h4 text-primary mb-0" id="{{ modal_id }}_title"></h2>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-3">
<p class="small text-muted mb-3 d-none" id="{{ modal_id }}_location"></p>
<div id="{{ modal_id }}_description" class="mb-0"></div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<a href="#" class="btn btn-primary d-none" id="{{ modal_id }}_link" target="_self">Read More</a>
</div>
</div>
</div>
</div>
{%- add_javascript inline eventDetailModalHelper = position:'body' -%}
window.mpOpenEventDetail = window.mpOpenEventDetail || function(options) {
var modalId = (options && options.modalId) || '{{ modal_id }}';
var modalEl = document.getElementById(modalId);
if (!modalEl || typeof bootstrap === 'undefined') {
return;
}
var titleEl = document.getElementById(modalId + '_title');
var whenEl = document.getElementById(modalId + '_when');
var locationEl = document.getElementById(modalId + '_location');
var descriptionEl = document.getElementById(modalId + '_description');
var linkEl = document.getElementById(modalId + '_link');
if (titleEl) {
titleEl.textContent = options.title || '';
}
if (whenEl) {
whenEl.textContent = options.when || '';
whenEl.classList.toggle('d-none', !options.when);
}
if (locationEl) {
locationEl.textContent = options.location || '';
locationEl.classList.toggle('d-none', !options.location);
}
if (descriptionEl) {
descriptionEl.innerHTML = options.descriptionHtml || '';
if (!options.descriptionHtml && options.description) {
descriptionEl.textContent = options.description;
}
}
if (linkEl) {
if (options.url) {
linkEl.href = options.url;
linkEl.classList.remove('d-none');
} else {
linkEl.classList.add('d-none');
linkEl.removeAttribute('href');
}
}
bootstrap.Modal.getOrCreateInstance(modalEl).show();
};
{%- end_javascript -%}
Loops upcoming events into rows and wires the modal triggers.
{% comment %}
Renders list of upcoming event cards
Inputs:
events
event_modal_id - optional modal id
{% endcomment -%}
<div class="row g-4">
{%- for event in events -%}
{%- include "/partials/_upcoming_event_row.liquid" event: event -%}
{%- endfor -%}
</div>
{%- add_javascript inline upcomingEventDetailTriggers = position:'body' -%}
document.addEventListener('click', function(e) {
var trigger = e.target.closest('.event-detail-trigger');
if (!trigger || typeof window.mpOpenEventDetail !== 'function') {
return;
}
e.preventDefault();
window.mpOpenEventDetail({
modalId: trigger.getAttribute('data-modal-id') || 'mpEventDetailModal',
title: trigger.getAttribute('data-title') || '',
when: trigger.getAttribute('data-when') || '',
location: trigger.getAttribute('data-location') || '',
descriptionHtml: trigger.getAttribute('data-description-html') || '',
url: trigger.getAttribute('data-url') || ''
});
});
{%- end_javascript -%}
One event card: date, title, and the button that opens the modal.
{% comment %}
Displays a single upcoming event as a card with date badge, description, and optional image.
Inputs:
event
img_preset_event_row - The image preset to use for the event image thumbnail
event_modal_id - optional modal id. Defaults to mpEventDetailModal
{% endcomment -%}
{%- var event_modal_id = event_modal_id | default: 'mpEventDetailModal' -%}
{%- var detail_url = '' -%}
{%- if event.has_url -%}
{%- set detail_url = event.full_url -%}
{%- elsif event.link.is_valid -%}
{%- var link_path = event.link.value -%}
{%- unless link_path == '/events' or link_path == '/events/' -%}
{%- set detail_url = link_path -%}
{%- endunless -%}
{%- endif -%}
{%- capture var event_when -%}
{%- if event.all_day -%}
{{ event.start_date | date: "MMMM d, yyyy" }}
{%- else -%}
{{ event.start_date | date: "MMMM d, yyyy · h:mm tt" }} {{ event.start_date | timezone }}
{%- endif -%}
{%- endcapture -%}
<div class="col-md-4">
<div class="card h-100 border-0 shadow-sm text-bg-light overflow-hidden">
{%- if event.image.is_valid -%}
{%- img event.image link:false preset:img_preset_event_row class:"card-img-top" style:"height: 180px; object-fit: cover;" -%}
{%- endif -%}
<div class="card-body p-4 d-flex flex-column">
<div class="d-flex align-items-start gap-3 mb-3">
<div class="text-bg-primary text-center rounded py-2 px-3 flex-shrink-0" style="min-width: 4.5rem;">
<span class="d-block h4 mb-0">{{ event.start_date | date: "dd" }}</span>
<span class="d-block small text-uppercase">{{ event.start_date | date: "MMM" }}</span>
</div>
<div class="flex-grow-1">
<p class="h5 mb-1">{{ event.title | html_decode }}</p>
</div>
</div>
{%- if event.description.is_valid -%}
<div class="small mb-2">{{ event.description }}</div>
{%- endif -%}
{%- if event.location.is_valid -%}
<p class="small text-muted mb-3"><i class="bi bi-geo-alt-fill me-1" aria-hidden="true"></i>{{ event.location | html_decode }}</p>
{%- endif -%}
<div class="mt-auto pt-2">
<button type="button"
class="btn btn-outline-primary btn-sm fw-semibold text-uppercase event-detail-trigger"
data-modal-id="{{ event_modal_id }}"
data-title="{{ event.title | html_decode | escape }}"
data-when="{{ event_when | strip | escape }}"
data-location="{{ event.location | html_decode | escape }}"
data-description-html="{{ event.description | escape }}"
{% if detail_url is_valid %}data-url="{{ detail_url | escape }}"{% endif %}>
Event Details
</button>
</div>
</div>
</div>
</div>