Challenge #12 – Fiesta Frenzy

11 July 2023 Solved Twig Intermediate

Atten­tion, party people! You’ve been summoned as the ulti­mate party plan­ner expert to unravel the wild Fiesta Frenzy! 

The sum­mer party team is in a tizzy, seek­ing your unbeat­able tal­ent for trop­ic­al fest­iv­it­ies to craft the per­fect sched­ule for the hot­test sum­mer spec­tacle of the year. Your mis­sion, should you choose to accept it, is to bring order to the chaos by metic­u­lously organ­ising and schedul­ing each event on the right date. Are you up for the challenge?

Cur­rently, the index.twig tem­plate con­tains two vari­ables that both dis­play the same event list. It is your task to ensure that the futureEvents vari­able only con­tains upcom­ing events, while the pastEvents vari­able con­tains the events that have already finished. 

Solution with dates

Please note that the cur­rent place­hold­er only dis­plays one date for each event. How­ever, it is import­ant to pay atten­tion to the fact that each event actu­ally has mul­tiple dates. 

When work­ing on your solu­tion, ensure that you prop­erly parse and handle the mul­tiple dates asso­ci­ated with each event, so that the com­plete set of dates is accur­ately reflec­ted in your final implementation.

This Git­Hub repo con­tains the Craft CMS site that you can spin up with a single com­mand, either loc­ally or in the browser using a Git­Hub Codespace (see the readme file in the repo).

Rules

Your solu­tion should out­put the events ordered in ascend­ing order for upcom­ing events and in des­cend­ing order for past events. This way, the events closest to the cur­rent date will appear at the top of the list.

  • You may only edit the templates/index.twig template.
  • You may over­ride the present­a­tion­al data in the pastEvents and futureEvents blocks in templates/index.twig
  • You may use Twig fil­ters or Col­lec­tion meth­ods to achieve the desired sor­ted result.
  • No plu­gins or mod­ules may be used except for the Craft Clos­ure plu­gin, which is already installed.
  • Optim­ise your code for read­ab­il­ity and main­tain­ab­il­ity (for future you).

Tips

Be clev­er about how you parse your dates to sim­pli­fy the sort­ing pro­cess and con­sider util­ising effi­cient tech­niques that can enhance your solution’s eleg­ance and effect­ive­ness. Explor­ing the power of col­lec­tion meth­ods could prove valu­able in achiev­ing a stream­lined and effi­cient approach.

Bonus Points

Bonus points will be awar­ded to the extreme party afi­cion­ados who also man­age to sort the parties by time!

Solution with times

Acknow­ledge­ments

Solution

The object­ive of this solu­tion is to organ­ise events based on their giv­en dates. This involves sev­er­al steps, start­ing with the cre­ation of a new object array. The array is struc­tured to include each event’s date and title. The sub­sequent pro­cess includes fil­ter­ing events into two cat­egor­ies – upcom­ing and past – fol­lowed by sort­ing each category.

  1. The ini­tial step is to cre­ate a flattened object array that com­bines rel­ev­ant data. This array will hold event titles, dates, and times. This is essen­tial to facil­it­ate fil­ter­ing and sort­ing later on.
  2. The col­lec­ted data is then cat­egor­ised into upcom­ing and past events. This cat­egor­isa­tion depends on the com­par­is­on of event dates with the cur­rent date. If an event’s date is earli­er than the cur­rent date, it’s clas­si­fied as a past event. How­ever, if the date is the same, the event’s time is also con­sidered to decide its category.
  3. Once cat­egor­ised, events with­in each group are sor­ted. Past events are sor­ted in des­cend­ing order, while future events are sor­ted in ascend­ing order. Sort­ing by both date and time ensures a coher­ent arrangement.
  4. Events are iter­ated through, and their titles, dates, and times are presen­ted in a clear and read­er-friendly format.

Lever­aging Twig Filters

When con­struct­ing our array with­in Twig, we must factor in the need for date cal­cu­la­tions right with­in our loop. This ensures the cor­rect place­ment of events into their des­ig­nated arrays, namely futureEvents or pastEvents. With­in this loop, we craft sev­er­al con­di­tion­als to assess wheth­er a date pre­cedes 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 intro­duce a second con­di­tion­al to see if our time is earli­er 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 con­di­tion­als haven’t been met we move on and add the event to the futureEvents array.

Pay atten­tion that in the con­di­tion­als there is a small gotcha”. To be able to com­pare the dates suc­cess­fully, we need to use the date func­tion to cre­ate a datetime object to allow us to com­pare with the now date object. Omit­ting the date func­tion can lead to unex­pec­ted 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 %}

Multisort fil­ter

Now that we have both arrays filled with the past and future events, we need to use a Twig fil­ter to add some magic. For this, we’ll use the multisort fil­ter. Note that this fil­ter doesn’t come stand­ard with Twig. It is a fil­ter that the lovely people from Pixel & Ton­ic added to make our life easi­er to handle sort­ing options on more com­plex arrays.

The multisort fil­ter will sort an array by one or more prop­er­ties or keys with­in that array’s val­ues. We choose to use multisort over sort because the multisort fil­ter allows us to sort on mul­tiple val­ues, where­as the sort fil­ter 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 accord­ing to the date first, fol­lowed by the time, which are the two para­met­ers we want to sort on. The multisort fil­ter sorts in ascend­ing order by default. This is the dir­ec­tion we want for the futureEvents array. How­ever, we want the passed events to be in des­cend­ing order. We can accom­plish this by adding the SORT_DESC parameter.

Once the above is com­pleted, we have our events split up and sor­ted accord­ingly – future events in ascend­ing order and past events in des­cend­ing order. So we can move on to our next step and dis­play the events where they belong.

Dis­play­ing events

Dis­play­ing the events is now reduced to noth­ing more than a for loop in which we parse the dates into a friendly format in the respect­ive 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 %}

Full Solu­tion

Com­bin­ing everything above, the com­pleted res­ult 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 %}

Sim­il­ar solu­tions: Ben Croker, Domin­ik Krulak.

Using Col­lec­tions

Things are sim­pler with col­lec­tions since once we’ve built out our col­lec­tion, we can lever­age the power of col­lec­tion meth­ods! So we’d need only a single vari­able at this early stage com­pared to two vari­ables, as in the clas­sic 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 %}

Using the col­lec­tion methods

As you can see in the above example, we have a clean and simple piece of code to cre­ate the col­lec­tion that we actu­ally need. Rather than work­ing with if/else con­di­tions, Lara­vel Col­lec­tions offer us a lot of col­lec­tion meth­ods. We will focus on three spe­cif­ic meth­ods in our solu­tion: reject, sortBy and sortByDesc.

Fil­ter­ing future and past dates

To fil­ter our col­lec­tion data to their respect­ive arrays, we will use the reject meth­od. We use it to reject any data that doesn’t pass our con­di­tion. Please note that to make this solu­tion work, you’d need the Craft Clos­ure plu­gin installed, which makes arrow func­tions (=>) avail­able to use every­where (rather than only in the filter, map and reduce Twig filters).

In the example, you’ll see that we cre­ate a vari­able that con­tains our rejec­tion” logic. To dis­play future events, we want to reject every item in which the date is older than the cur­rent date. If the date is the same, we com­pare the time of today. For past events, we simply reject any­thing that is in the future, and the con­di­tion is basic­ally a reverse of the rejectPast clos­ure 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 res­ult for both arrays would now look some­thing like:

{% set futureEvents = eventCollectionFlat.reject(rejectPast) %}
{% set pastEvents = eventCollectionFlat.reject(rejectFuture) %}

Clean and simple, right?

Sort­ing the events

Finally, we will lever­age the sortBy and sort­By­Desc col­lec­tion meth­ods. The nicety about these meth­ods is that their names speak for them­selves, only improv­ing the read­ab­il­ity of your code. Both meth­ods can con­tain one or more para­met­ers to sort on, just like the multisort fil­ter that we used pre­vi­ously. The use­ful part about col­lec­tion meth­ods is that they can be chained just like you’d do with entry quer­ies, keep­ing the code clean and con­cise, mean­ing we can add them after the reject method. 

{% set futureEvents = eventCollectionFlat.reject(rejectPast).sortBy('date', 'time') %}
{% set pastEvents = eventCollectionFlat.reject(rejectFuture).sortByDesc('date', 'time') %}

Dis­play­ing events

To dis­play the events, we can use the same logic as the Twig fil­ter 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 %}

Full Solu­tion

Com­bin­ing everything above, the com­pleted res­ult 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 %}

Submitted Solutions

  • Liam Rella
  • Dominik Krulak
  • Ben Croker