Listing management: fields, sorting, and availability.
Listings come in two types:
- Standard — fixed capacity with optional date
- Daily — date-based booking with holiday exclusions
Add days to a YYYY-MM-DD date string
Add N months to an ISO timestamp, clamping to the last day of the target month. e.g. 2026-01-31 + 1mo → 2026-02-28 Preserves the time component (hour/minute/second/ms). Zero months returns the input with canonical ISO string formatting.
The human-readable label for one booking's actual span: the stored
[date, endDate) range when a multi-day range is stored, the listing's
fixed duration when only a start date is (legacy rows written before end
dates were stored), else the single booked day. "" when there is no date.
The ONE booked-range renderer the confirmation email, the /t ticket cards,
and the collapsed package displays share, so they can never disagree about
a booking's stay.
The whole day count of a stored [start_at, end_at) booking range — the
customisable day count the buyer chose. A missing or degenerate range is 1.
Build the calendar grid for a YYYY-MM month as a flat list of YYYY-MM-DD strings. The grid is whole Monday→Sunday weeks spanning the month plus one extra full week on each side, so adjacent-month context is always visible.
Generate a range of YYYY-MM-DD date strings from start to end (inclusive)
Compute how many days ago an listing started, relative to today in the configured timezone. Returns null if the listing date is today or in the future, or if the date is empty/invalid. For past listings, returns a positive integer (1 = yesterday).
Format a YYYY-MM-DD date for display. Returns "Monday 15 March 2026"
Format a UTC ISO datetime as a date-only label in the configured timezone, e.g. "Monday 15 June 2026" — no time. Returns "" for an empty/invalid input. Used where a stored timestamp should read as a plain published date (the public news post page).
Format a booking's stored [start_at, end_at) ISO range as a human label.
1-day bookings collapse to formatDateLabel; multi-day bookings use the
compact English range formatter (inclusive — subtracts 1 day from end_at,
which is the first midnight after the booked window).
Compact English date-range formatter. Uses an en dash (–) for ranges.
Format an ISO datetime string for display in the given timezone. Returns e.g. "Monday 15 June 2026 at 14:00 BST"
Compact ISO datetime formatter for table cells. Returns e.g. "07/04/2026 14:00" in the configured timezone.
Format a YYYY-MM month string for display, e.g. "July 2026".
Human "time ago" label for a past ISO timestamp, relative to nowMsValue
(epoch ms), via Intl.RelativeTimeFormat in the largest whole unit that
applies — e.g. "now", "5 minutes ago", "yesterday", "2 days ago". Returns
null for an unparseable or future timestamp.
Compute available booking dates for a daily listing.
Filters by bookable days of the week and excludes holidays.
For listings with duration_days > 1, excludes start dates whose full range
would hit a non-bookable day or extend past the booking window.
Available start dates for a daily listing's booking/date pickers. Customisable-days listings use single-day availability — the span is chosen separately and validated at submit time — so every individually-bookable start is offered; other listings use their fixed duration.
Get the next available booking date for a daily listing. More efficient than getAvailableDates()[0] — stops at first match. Returns null if no bookable dates are available.
Whether booking days consecutive days starting on date is valid for a
daily listing: every day must be a bookable weekday, fall outside all
holidays, and stay within the listing's booking window. Used to enforce the
visitor's chosen span on "customisable days" listings at submit time, where
the day count isn't known when the date list is rendered.
Convert a UTC ISO datetime to a YYYY-MM-DD calendar date in the given timezone. Returns null if the input is empty or invalid. Used by the calendar view to map standard listing dates to calendar days.
Load all listings with holidays and return them sorted. With a filter, only the listings it keeps are returned; without one, every listing is returned.
Determine which contact fields to collect for multiple listings. Returns the union of all field settings, sorted by canonical CONTACT_FIELDS order.
List every YYYY-MM month within yearsEitherSide years of the given month's
year, in ascending order. e.g. monthsAround("2026-03", 5) runs from
"2021-01" to "2031-12". Used to populate the calendar's month picker.
Normalize datetime-local "YYYY-MM-DDTHH:MM" to full UTC ISO string. The input is interpreted as local time in the given timezone and converted to UTC.
Parse a date/timestamp string to epoch milliseconds, or null when it
doesn't parse — the safe wrapper around Date.parse, whose own failure
mode (NaN) is easy to let leak into arithmetic by accident.
Parse a user-supplied YYYY-MM-DD value (a ?date= query param): the string
when well-formed and a real calendar date, else null. The UTC round-trip
rejects rolled-over impossibilities like 2025-02-30.
Parse a comma-separated fields string into individual ContactField names
Shift a YYYY-MM month string by delta months (negative goes backwards).
Crosses year boundaries: shiftMonth("2026-12", 1) → "2027-01".
Sort listings in unified 3-tier order. Takes anything carrying the values the order is built from, so a narrow picker read sorts the same way a full listing record does.
Round a date down to the start of the current hour for cache-stable signatures
The dated entry whose booked range ends last — the stay covering a whole package bundle — or null when every entry is date-less (a standard package). A dated entry with no stored end (a single-day booking, or a legacy row) sorts below any ranged stay. Shared by the collapsed email/SVG displays and the /t package card, so every surface picks the SAME representative stay.
Ensure "email" is included in an listing fields setting
Usage
import * as mod from "docs/listings.ts";