Represents a calendar event.
Consumers map their database records to this struct before passing them
to calendar components. Only id and start are required — everything
else has sensible defaults.
End time semantics
End times are exclusive (half-open interval [start, end)).
- An all-day event on April 1st:
start: ~D[2026-04-01], end: ~D[2026-04-02] - A 1-hour meeting at 10am:
start: ~U[2026-04-01 10:00:00Z], end: ~U[2026-04-01 11:00:00Z]
If end is nil, a default duration is applied:
- All-day events default to 1 day
- Timed events default to 30 minutes
Examples
# Minimal event
%PhoenixLiveCalendar.Event{id: "1", start: ~D[2026-04-01]}
# Timed event with details
%PhoenixLiveCalendar.Event{
id: "meeting-1",
title: "Team Standup",
start: ~U[2026-04-01 09:00:00Z],
end: ~U[2026-04-01 09:30:00Z],
color: "bg-primary"
}
# All-day event spanning multiple days
%PhoenixLiveCalendar.Event{
id: "vacation-1",
title: "Spring Break",
start: ~D[2026-04-06],
end: ~D[2026-04-11],
all_day: true
}
# Booking with resource and constraints
%PhoenixLiveCalendar.Event{
id: "booking-1",
title: "Dr. Smith - Consultation",
start: ~U[2026-04-01 14:00:00Z],
end: ~U[2026-04-01 15:00:00Z],
resource_id: "room-a",
editable: false,
overlap: false,
extra: %{patient_id: "p-123", type: :consultation}
}Visibility tiers
The visibility field controls which views an event appears in.
Higher values mean the event appears in more zoomed-out views.
Uses multiples of 10 for granularity between tiers.
| Visibility | Shows in | Example use |
|---|---|---|
| 10 | Day only | Lunch breaks, focus time |
| 20 | Day + Week (default) | Regular meetings |
| 25 | Day + Week | Slightly important |
| 30 | Day + Week + Month | Key deadlines |
| 35 | Day + Week + Month | High priority |
| 40 | Day + Week + Month + Year | Company milestones |
The CalendarComponent applies view-specific thresholds automatically.
Override with min_visibility attribute on the component.
Summary
Functions
Returns whether this event is an all-day event.
Whether two events occupy any calendar date in common (INCLUSIVE
first/last dates — the occupancy rule on_date?/2 uses, so a
midnight-crossing event counts on its spill-over day).
The times this event's block occupies on ONE day, clipped to the visible
[min_time, max_time] window — or nil when nothing of it is visible
that day. The single source of truth for per-day time-grid segments
Returns the duration of the event in seconds.
Returns the effective end time/date for this event.
The FIRST calendar date this event occupies (its start date).
Whether the event occupies any date in [range_start, range_end) —
inclusive start, EXCLUSIVE end, the same shape on_date_range_change
reports and DateHelpers.visible_range/3 returns.
The LAST calendar date this event occupies on a date grid (inclusive).
Returns whether this event spans multiple days.
Returns whether this event falls on the given date.
Whether the event belongs on a resource's row/column — matches the
singular resource_id OR membership in the plural resource_ids.
Returns whether this event overlaps with the given date range [range_start, range_end).
Returns whether this event occupies more than one calendar DATE — i.e. its first and last occupied days differ.
Returns whether this event meets a minimum visibility threshold.
Types
@type display() :: :auto | :background | :inverse_background | :none
@type priority() :: :low | :normal | :high | :urgent
@type status() :: :confirmed | :tentative | :cancelled | :pending_approval | :no_show
@type t() :: %PhoenixLiveCalendar.Event{ all_day: boolean(), badge: String.t() | nil, border_color: String.t() | nil, category: String.t() | atom() | nil, class: String.t() | nil, color: String.t() | atom() | nil, description: String.t() | nil, display: display(), editable: boolean(), end: Date.t() | DateTime.t() | NaiveDateTime.t() | nil, extra: map(), group_id: term() | nil, icon: String.t() | nil, id: term(), layer_id: term() | nil, location: String.t() | nil, overlap: boolean(), priority: priority(), recurrence_id: term() | nil, resource_id: term() | nil, resource_ids: [term()] | nil, rrule: String.t() | nil, start: Date.t() | DateTime.t() | NaiveDateTime.t(), status: status(), text_color: String.t() | nil, title: String.t() | nil, transparency: transparency(), urgency: urgency(), url: String.t() | nil, visibility: pos_integer() }
@type transparency() :: :opaque | :transparent
@type urgency() :: :none | :attention | :warning | :critical
Functions
Returns whether this event is an all-day event.
An event is considered all-day if all_day is true or if
start is a Date (not a DateTime or NaiveDateTime).
Whether two events occupy any calendar date in common (INCLUSIVE
first/last dates — the occupancy rule on_date?/2 uses, so a
midnight-crossing event counts on its spill-over day).
The times this event's block occupies on ONE day, clipped to the visible
[min_time, max_time] window — or nil when nothing of it is visible
that day. The single source of truth for per-day time-grid segments:
- a midnight-crossing event runs to end-of-day on its first day and from 00:00 on its last (the raw EXCLUSIVE end date decides the split, so an event ending exactly at midnight still renders on its start day)
- all-day events span the whole visible window
Returns the duration of the event in seconds.
For all-day events, returns the number of days multiplied by 86400.
@spec effective_end(t()) :: Date.t() | DateTime.t() | NaiveDateTime.t()
Returns the effective end time/date for this event.
If end is nil, applies a default duration:
- All-day events: start + 1 day
- Timed events: start + 30 minutes
The FIRST calendar date this event occupies (its start date).
Whether the event occupies any date in [range_start, range_end) —
inclusive start, EXCLUSIVE end, the same shape on_date_range_change
reports and DateHelpers.visible_range/3 returns.
Uses the same occupancy rule as on_date?/2/last_date/1, so a timed
event running past midnight counts on its spill-over day and an event
ending exactly at midnight does not.
The LAST calendar date this event occupies on a date grid (inclusive).
This is the single source of truth for "which day is the event's last day", so bar rendering and occupancy never disagree:
- All-day events:
endis exclusive, so the last day isend - 1. - Timed events: the event occupies the date it ends ON (an event ending 10:30 on the 17th is on the 17th) — UNLESS it ends exactly at midnight (00:00:00), the boundary, which does not count as occupying that day.
Returns whether this event spans multiple days.
Returns whether this event falls on the given date.
Whether the event belongs on a resource's row/column — matches the
singular resource_id OR membership in the plural resource_ids.
@spec overlaps_range?(t(), Date.t() | DateTime.t(), Date.t() | DateTime.t()) :: boolean()
Returns whether this event overlaps with the given date range [range_start, range_end).
Returns whether this event occupies more than one calendar DATE — i.e. its first and last occupied days differ.
This is the right test for "render as one continuous bar across day cells"
on a date grid: it is true for a multi-day all-day event AND for a timed
event that runs past midnight into another date (a 10pm→2am event is on two
dates), but false for a same-day event or one that ends exactly at midnight
(which occupies only the starting day). Unlike multi_day?/1, it doesn't
care how many hours the event lasts — only which dates it touches.
@spec visible_at?(t(), pos_integer()) :: boolean()
Returns whether this event meets a minimum visibility threshold.
Events with visibility >= min_visibility are considered visible.
Default event visibility is 20 (shows in day and week views).
View defaults
| View | Threshold | Shows events with visibility >= |
|---|---|---|
| Day | 10 | 10+ (almost everything) |
| Week | 20 | 20+ (default events) |
| Month | 30 | 30+ (important only) |
| Year | 40 | 40+ (highest importance) |
Examples
iex> Event.visible_at?(%Event{id: 1, start: ~D[2026-04-01], visibility: 20}, 30)
false
iex> Event.visible_at?(%Event{id: 1, start: ~D[2026-04-01], visibility: 30}, 30)
true