Packages search and calendar
The packages_search_and_calendar tag provides a calendar-based way to search for and display package availability.
Key feature: When a date is selected in calendar view, you can fetch available start times with aggregated availability and pricing information, allowing customers to drill down from date → start time → specific packages.
Basic Usage
input
{% packages_search_and_calendar packages: package_array %}
<div class="calendar">
{% for date in calendar.dates %}
<div class="day">
{{ date.start_on | date: "%A, %b %d" }}
{% if date.has_start_times and date.sold_out == false %}
<a href="?search[departure_date][equal_to]={{ date.start_on }}">{{ date.start_on }}</a> <!--Clickable date, pass other search params as needed -->
{% else %}
<p>{{ date.start_on }}</p> <!--Disabled date-->
{% endif %}
</div>
{% endfor %}
</div>
{% endpackages_search_and_calendar %}
Parameters
packages
Type: Array of Packages, or Package IDs
The only required parameter. Limit the search to specific packages. You can pass an array of package objects, or an array of package IDs.
{% packages_search_and_calendar packages: package_array %}
guest_count
Type: Integer
Default: Minimum guest count across included packages
The number of guests to check availability for. This affects both availability checking and pricing calculations.
{% packages_search_and_calendar guest_count: 4 %}
month
Type: Integer (1-12)
The month to display in the calendar view. Used together with year to control which month is shown.
{% packages_search_and_calendar month: 6, year: 2025 %}
year
Type: Integer
The year to display in the calendar view. Used together with month to control which month is shown.
{% packages_search_and_calendar month: 6, year: 2025 %}
departure_date
Type: Object
Accepts an object which specifies how to handle the search through equal_to, greater_than, greater_or_equal_than or less_than each taking a date in SQL format YYYY-MM-DD. The date(s) can be passed as a Liquid variable or explicitly. When set, result.start_times will only include start times for the filtered dates.
{% packages_search_and_calendar departure_date: departure_date_object %}
When using query params, pass the date as:
?search[departure_date][equal_to]=2025-06-15
page_size
Type: Integer (max 100)
Default: 5
Number of start time results per page.
{% packages_search_and_calendar page_size: 20 %}
exclude_sold_out_products
Type: Boolean
Default: false
When true, excludes packages that are sold out from the results.
{% packages_search_and_calendar exclude_sold_out_products: true %}
include_organisation_packages
Type: Boolean
Default: false
When true, allows searching for packages belonging to any company in the current company’s organisation.
{% packages_search_and_calendar include_organisation_packages: true %}
Search Params
Search params can be handled in one of two ways:
Passing explicit attributes
You can pass any of the parameters listed above directly as attributes in the tag itself. This is useful for building a page with specific search criteria.
{% packages_search_and_calendar
packages: package_array,
guest_count: 4,
month: 6,
year: 2025,
exclude_sold_out_products: true,
include_organisation_packages: true
%}
As query params
It’s also possible to pass search params as query params. This is useful when building dynamic search pages.
Any of the above parameters can be used as a query param and should all be namespaced to search.
For example:
/packages?search[guest_count]=4&search[month]=6&search[year]=2025&search[departure_date][equal_to]=2025-06-15
Params passed through a query param will override any of those specified as an attribute.
Accessing the query params
Once the search has been executed, you can get a reference to the params and values using the search object, which exposes all of the params listed in the Parameters section.
{{ search.guest_count }}
{{ search.month }}
{{ search.year }}
{{ search.departure_date.equal_to }}
{{ search.exclude_sold_out_products }}
{{ search.include_organisation_packages }}
Objects Exposed
calendar
The calendar object contains information about the current calendar view and its dates.
date object
Each date in calendar.dates represents one day with aggregated availability information across all matching packages.
date.start_on
Date
The date in YYYY-MM-DD format.
date.has_start_times
Boolean
Whether there are any packages starting on this date.
date.sold_out
Boolean
Whether all packages across all start times on this date are sold out.
result
The result object contains list view data with pagination.
result.start_times
Array of start_time objects
Array of start time objects with availability and pricing. See start_time object below.
start_time object
Each start time represents a specific time slot with aggregated availability information across all matching packages.
start_time.time
String (HH:MM)
The start time in HH:MM format.
start_time.date
Date
The start date for this start time.
start_time.sold_out
Boolean
Whether the packages on this start time are sold out.
start_time.price
The minimum price across available packages for the specified guest count.
start_time.starting_stock
Integer or nil
The total initial stock across all packages. Returns nil if any package has infinite stock.
start_time.remaining_stock
Integer or nil
The total remaining stock across all packages. Returns nil if any package has infinite stock.
paginate
Standard pagination object for list view results.
search
The search object provides access to current search parameters.
Example
This example demonstrates using query params to control the calendar view and Turbo to load start times when a date is selected.
input
<turbo-frame id="package-availability">
{% packages_search_and_calendar packages: package_array %}
<nav class="month-navigation">
<a href="?search[month]={{ calendar.prev.month }}&search[year]={{ calendar.prev.year }}&search[guest_count]={{ search.guest_count }}" aria-label="Navigate to previous month">
← {{ calendar.prev.first_day | date: "%B %Y" }}
</a>
<h2>{{ calendar.first_day | date: "%B %Y" }}</h2>
<a href="?search[month]={{ calendar.next.month }}&search[year]={{ calendar.next.year }}&search[guest_count]={{ search.guest_count }}" aria-label="Navigate to next month">
{{ calendar.next.first_day | date: "%B %Y" }} →
</a>
</nav>
<div class="calendar-grid">
{% for date in calendar.dates %}
<div class="calendar-day {% unless date.has_start_times %}unavailable{% endunless %}">
<span class="day-number">{{ date.start_on | date: "%d" }}</span>
{% if date.has_start_times and date.sold_out == false %}
<a href="?search[departure_date][equal_to]={{ date.start_on }}&search[guest_count]={{ search.guest_count }}" class="calendar-date-link">
Select
</a>
{% elsif date.sold_out %}
<span class="sold-out">Sold out</span>
{% endif %}
</div>
{% endfor %}
</div>
<div class="start-times-results">
{% if result.start_times %}
<h3>Available Start Times for {{ search.departure_date.equal_to | date: "%A, %B %d, %Y" }}</h3>
{% for start_time in result.start_times %}
<div class="start-time-card">
<strong>{{ start_time.time }}</strong>
{% unless start_time.sold_out %}
<span class="price">{{ start_time.price | money }}pp</span>
<a href="/booking?date={{ start_time.date }}&start_time={{ start_time.time }}">Book Now</a>
{% else %}
<span class="sold-out">Sold out</span>
{% endunless %}
</div>
{% endfor %}
{% endif %}
</div>
{% endpackages_search_and_calendar %}
</turbo-frame>
{% render 'some-scripts-to-handle-turbo' %}
How it works:
- The entire calendar is wrapped in a
<turbo-frame>with a unique ID - Month navigation links update the
search[month]andsearch[year]query params, which Turbo intercepts and updates the frame - When a date is clicked, the
search[departure_date][equal_to]param is set, which returns the same page withresult.start_timespopulated for that date - Turbo automatically updates only the content within the frame, creating a seamless user experience