Attention, party people! You’ve been summoned as the ultimate party planner expert to unravel the wild Fiesta Frenzy!
The summer party team is in a tizzy, seeking your unbeatable talent for tropical festivities to craft the perfect schedule for the hottest summer spectacle of the year. Your mission, should you choose to accept it, is to bring order to the chaos by meticulously organising and scheduling each event on the right date. Are you up for the challenge?
Currently, the index.twig template contains two variables that both display the same event list. It is your task to ensure that the futureEvents
variable only contains upcoming events, while the pastEvents
variable contains the events that have already finished.
Please note that the current placeholder only displays one date for each event. However, it is important to pay attention to the fact that each event actually has multiple dates.
When working on your solution, ensure that you properly parse and handle the multiple dates associated with each event, so that the complete set of dates is accurately reflected in your final implementation.
This GitHub repo contains the Craft CMS site that you can spin up with a single command, either locally or in the browser using a GitHub Codespace (see the readme file in the repo).
Your solution should output the events ordered in ascending order for upcoming events and in descending order for past events. This way, the events closest to the current date will appear at the top of the list.
templates/index.twig
template.pastEvents
and futureEvents
blocks in templates/index.twig
Be clever about how you parse your dates to simplify the sorting process and consider utilising efficient techniques that can enhance your solution’s elegance and effectiveness. Exploring the power of collection methods could prove valuable in achieving a streamlined and efficient approach.
Bonus points will be awarded to the extreme party aficionados who also manage to sort the parties by time!
The objective of this solution is to organise events based on their given dates. This involves several steps, starting with the creation of a new object array. The array is structured to include each event’s date and title. The subsequent process includes filtering events into two categories – upcoming and past – followed by sorting each category.
When constructing our array within Twig, we must factor in the need for date calculations right within our loop. This ensures the correct placement of events into their designated arrays, namely futureEvents
or pastEvents
. Within this loop, we craft several conditionals to assess whether a date precedes the present date. Should this hold true, the event is routed to the pastEvents
array.
But we aren’t done yet! Of course, we also want to add events on the same day where the time of the event has already elapsed. To address this, we introduce a second conditional to see if our time is earlier than or equal to the present time. If that’s the case, the event itself will also be pushed into our pastEvents
array. If both conditionals haven’t been met we move on and add the event to the futureEvents
array.
Pay attention that in the conditionals there is a small “gotcha”. To be able to compare the dates successfully, we need to use the date
function to create a datetime
object to allow us to compare with the now
date object. Omitting the date function can lead to unexpected outcomes!
{% set pastEvents = [] %}
{% set futureEvents = [] %}
{% for event in events %}
{% for item in event.dates %}
{% set simplifiedEvent = {
title: event.title,
date: item.date,
time: item.time,
} %}
{% if item.date|date('Y-m-d') < now|date('Y-m-d') %}
{% set pastEvents = pastEvents|push(simplifiedEvent) %}
{% elseif item.date|date('Y-m-d') == now|date('Y-m-d') and item.time|time('H:i') < now|time('H:i') %}
{% set pastEvents = pastEvents|push(simplifiedEvent) %}
{% else %}
{% set futureEvents = pastEvents|push(simplifiedEvent) %}
{% endif %}
{% endfor %}
{% endfor %}
Now that we have both arrays filled with the past and future events, we need to use a Twig filter to add some magic. For this, we’ll use the multisort filter. Note that this filter doesn’t come standard with Twig. It is a filter that the lovely people from Pixel & Tonic added to make our life easier to handle sorting options on more complex arrays.
The multisort
filter will sort an array by one or more properties or keys within that array’s values. We choose to use multisort
over sort
because the multisort
filter allows us to sort on multiple values, whereas the sort
filter only accepts a single value.
Let’s get into it:
{% set pastEvents = pastEvents|multisort(['date', 'time'], SORT_DESC) %}
{% set futureEvents = futureEvents|multisort(['date', 'time']) %}
As you can see, we sort according to the date first, followed by the time, which are the two parameters we want to sort on. The multisort
filter sorts in ascending order by default. This is the direction we want for the futureEvents
array. However, we want the passed events to be in descending order. We can accomplish this by adding the SORT_DESC
parameter.
Once the above is completed, we have our events split up and sorted accordingly – future events in ascending order and past events in descending order. So we can move on to our next step and display the events where they belong.
Displaying the events is now reduced to nothing more than a for
loop in which we parse the dates into a friendly format in the respective blocks:
{% block futureEvents %}
{% for event in futureEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
{% block pastEvents %}
{% for event in pastEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
Combining everything above, the completed result is as follows.
{% set events = craft.entries
.section('events')
.all()
%}
{% set pastEvents = [] %}
{% set futureEvents = [] %}
{% for event in events %}
{% for item in event.dates %}
{% set simplifiedEvent = {
title: event.title,
date: item.date,
time: item.time,
} %}
{% if item.date|date('Y-m-d') < now|date('Y-m-d') %}
{% set pastEvents = pastEvents|push(simplifiedEvent) %}
{% elseif item.date|date('Y-m-d') == now|date('Y-m-d') and item.time|time('H:i') < now|time('H:i') %}
{% set pastEvents = pastEvents|push(simplifiedEvent) %}
{% else %}
{% set futureEvents = pastEvents|push(simplifiedEvent) %}set futureEvent
{% endif %}
{% endfor %}
{% endfor %}
{% set pastEvents = pastEvents|multisort(['date', 'time'], SORT_DESC) %}
{% set futureEvents = futureEvents|multisort(['date', 'time']) %}
{% block futureEvents %}
{% for event in futureEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
{% block pastEvents %}
{% for event in pastEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
Similar solutions: Ben Croker, Dominik Krulak.
Things are simpler with collections since once we’ve built out our collection, we can leverage the power of collection methods! So we’d need only a single variable at this early stage compared to two variables, as in the classic Twig way.
{% set eventCollectionFlat = collect([]) %}
{% for event in events %}
{% for item in event.dates %}
{% set eventCollectionFlat = eventCollectionFlat.push({
title: event.title,
date: item.date,
time: item.time,
}) %}
{% endfor %}
{% endfor %}
As you can see in the above example, we have a clean and simple piece of code to create the collection that we actually need. Rather than working with if/else
conditions, Laravel Collections offer us a lot of collection methods. We will focus on three specific methods in our solution: reject
, sortBy
and sortByDesc
.
To filter our collection data to their respective arrays, we will use the reject method. We use it to reject any data that doesn’t pass our condition. Please note that to make this solution work, you’d need the Craft Closure plugin installed, which makes arrow functions (=>
) available to use everywhere (rather than only in the filter
, map
and reduce
Twig filters).
In the example, you’ll see that we create a variable that contains our “rejection” logic. To display future events, we want to reject every item in which the date is older than the current date. If the date is the same, we compare the time of today. For past events, we simply reject anything that is in the future, and the condition is basically a reverse of the rejectPast
closure we defined.
{% set rejectPast = (value) => (value.date|date('Y-m-d') < now|date('Y-m-d')) or (value.date|date('Y-m-d') == now|date('Y-m-d') and value.time|time('H:i') < now|time('H:i')) %}
{% set rejectFuture = (value) => (value.date|date('Y-m-d') > now|date('Y-m-d')) or (value.date|date('Y-m-d') == now|date('Y-m-d') and value.time|time('H:i') > now|time('H:i')) %}
The result for both arrays would now look something like:
{% set futureEvents = eventCollectionFlat.reject(rejectPast) %}
{% set pastEvents = eventCollectionFlat.reject(rejectFuture) %}
Clean and simple, right?
Finally, we will leverage the sortBy and sortByDesc collection methods. The nicety about these methods is that their names speak for themselves, only improving the readability of your code. Both methods can contain one or more parameters to sort on, just like the multisort
filter that we used previously. The useful part about collection methods is that they can be chained just like you’d do with entry queries, keeping the code clean and concise, meaning we can add them after the reject
method.
{% set futureEvents = eventCollectionFlat.reject(rejectPast).sortBy('date', 'time') %}
{% set pastEvents = eventCollectionFlat.reject(rejectFuture).sortByDesc('date', 'time') %}
To display the events, we can use the same logic as the Twig filter example.
{% block futureEvents %}
{% for event in futureEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
{% block pastEvents %}
{% for event in pastEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
Combining everything above, the completed result is as follows.
{% set events = craft.entries
.section('events')
.collect()
.all()
%}
{% set eventCollectionFlat = collect([]) %}
{% for event in events %}
{% for item in event.dates %}
{% set eventCollectionFlat = eventCollectionFlat.push({
'title': event.title,
'date': item.date,
'time': item.time,
}) %}
{% endfor %}
{% endfor %}
{% set rejectPast = (value) => (value.date|date('Y-m-d') < now|date('Y-m-d')) or (value.date|date('Y-m-d') == now|date('Y-m-d') and value.time|time('H:i') < now|time('H:i')) %}
{% set rejectFuture = (value) => (value.date|date('Y-m-d') > now|date('Y-m-d')) or (value.date|date('Y-m-d') == now|date('Y-m-d') and value.time|time('H:i') > now|time('H:i')) %}
{% set futureEvents = eventCollectionFlat.reject(rejectPast).sortBy('date') %}
{% set pastEvents = eventCollectionFlat.reject(rejectFuture).sortByDesc('date') %}
{% block futureEvents %}
{% for event in futureEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
{% block pastEvents %}
{% for event in pastEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
Solution submitted by Liam Rella on 24 July 2023.
{% extends '_layout.twig' %}
{% set events = craft.entries
.section('events')
.collect()
%}
{% set today = now|date('U') %}
{% set futureEvents = collect([]) %}
{% set pastEvents = collect([]) %}
{% for event in events %}
{% for session in event.dates %}
{# build a timestamp that is a combination of the date and time column #}
{% set date = session.date|date('Y-m-d') %}
{% set time = session.time|date('H:i:s') %}
{% set timestamp = "#{date} #{time}"|date('U') %}
{# set our new event data structure #}
{# {
title: Booty Balooza,
timestamp: 1688252400
} #}
{% set eventData = {title: event.title, timestamp: timestamp} %}
{# split events into past and future sessions #}
{% if timestamp > today %}
{% set futureEvents = futureEvents.push(eventData) %}
{% else %}
{% set pastEvents = pastEvents.push(eventData) %}
{% endif %}
{% endfor %}
{% endfor %}
{# sort events by timestamps and limit to 8 events in each list #}
{% set futureEvents = futureEvents.sortBy('timestamp').take(8) %}
{% set pastEvents = pastEvents.sortByDesc('timestamp').take(8) %}
{%- block futureEvents -%}
{%- for event in futureEvents -%}
<div>
<h3 class="text-lg font-bold">
{{- event.title -}}
</h3>
<span class="text-sm text-gray-700">
{{- [event.timestamp|date('F jS Y'), 'at', event.timestamp|date('g:i A')]|join(' ') -}}
</span>
</div>
{%- endfor -%}
{%- endblock -%}
{%- block pastEvents -%}
{%- for event in pastEvents -%}
<div>
<h3 class="text-lg font-bold">
{{- event.title -}}
</h3>
<span class="text-sm text-gray-700">
{{- [event.timestamp|date('F jS Y'), 'at', event.timestamp|date('g:i A')]|join(' ') -}}
</span>
</div>
{%- endfor -%}
{%- endblock -%}
Solution submitted by Dominik Krulak on 29 July 2023.
{% extends '_layout.twig' %}
{### The goal is to show future and past events based on table field `dates`
### and sort them closest to a current date and time.
###}
{% set events = craft.entries
.section('events')
.all()
%}
{# Prepare array-like object for future and past events #}
{% set futureEvents = {} %}
{% set pastEvents = {} %}
{# Build object arrays for future and past events like this:
{# [
{# 0: {'entry': event, 'date': date, 'time': time},
{# 1: {'entry': event, 'date': date, 'time': time},
{# 2: {'entry': event, 'date': date, 'time': time},
{# ]
#}
{% for event in events %}
{% set index = loop.index0 %}
{% set dates = event.dates %}
{% for row in dates %}
{% set date = row.date %}
{% set time = row.time %}
{# Use 'date' function to build a date out of `date` and `time` variables
to allow us comparision with `now` date object #}
{% set eventDate = {
date: date|date('Y-m-d'),
time: time|date('H:i'),
} %}
{# If page is loaded exactly at the current time when event starts
we consider it a future event #}
{# Future events - use 'multisort' filter to sort first by date and than time #}
{% if date(eventDate) >= now %}
{% set futureEvents = futureEvents|merge({
(index): {
'entry': event,
'date': date,
'time': time
}
})|multisort(['date', 'time']) %}
{# Past events - use 'multisort' filter to sort first by date and than time
and sort it descendently #}
{% else %}
{% set pastEvents = pastEvents|merge({
(index): {
'entry': event,
'date': date,
'time': time
}
})|multisort(['date', 'time'], direction=SORT_DESC) %}
{% endif %}
{% endfor %}
{% endfor %}
{%- block futureEvents -%}
{%- for event in futureEvents -%}
<div>
<h3 class="text-lg font-bold">
{{- event.entry.title -}}
</h3>
<span class="text-sm text-gray-700">
{{- event.date|date('F jS Y') }} at {{ event.time|time('short') -}}
</span>
</div>
{%- endfor -%}
{%- endblock -%}
{%- block pastEvents -%}
{%- for event in pastEvents -%}
<div>
<h3 class="text-lg font-bold">
{{- event.entry.title -}}
</h3>
<span class="text-sm text-gray-700">
{{- event.date|date('F jS Y') }} at {{ event.time|time('short') -}}
</span>
</div>
{%- endfor -%}
{%- endblock -%}
Solution submitted by Ben Croker on 31 July 2023.
{% extends '_layout.twig' %}
{% set events = craft.entries
.section('events')
.all()
%}
{% set pastEvents = [] %}
{% set futureEvents = [] %}
{% for entry in events %}
{% for row in entry.dates %}
{% set event = {
entry: entry,
date: row.date,
time: row.time,
} %}
{% if row.date|date('Y-m-d') < now|date('Y-m-d') %}
{% set pastEvents = pastEvents|push(event) %}
{% elseif row.date|date('Y-m-d') == now|date('Y-m-d') and row.time|time('H:i') < now|time('H:i') %}
{% set pastEvents = pastEvents|push(event) %}
{% else %}
{% set futureEvents = futureEvents|push(event) %}
{% endif %}
{% endfor %}
{% endfor %}
{% set pastEvents = pastEvents|multisort(['date', 'time'], SORT_DESC) %}
{% set futureEvents = futureEvents|multisort(['date', 'time']) %}
{% block futureEvents %}
{% for event in futureEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.entry.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}
{% block pastEvents %}
{% for event in pastEvents %}
<div>
<h3 class="text-lg font-bold">
{{ event.entry.title }}
</h3>
<span class="text-sm text-gray-700">
{{ event.date|date('F jS Y') }} at {{ event.time|time('short') }}
</span>
</div>
{% endfor %}
{% endblock %}