defmodule <%= @module_name %>.Components.UI.DateRangePicker do
@moduledoc """
Date range picker with dual-calendar server-side rendering.
Ejected from PhiaUI — you own this copy. Customise freely.
## Example
<.date_range_picker
id="booking-range"
view_month={@view_month}
from={@date_from}
to={@date_to}
on_change="select-date"
on_month_change="change-month"
min_date={Date.utc_today()}
/>
## LiveView handlers
def handle_event("select-date", %{"date" => date_str}, socket) do
date = Date.from_iso8601!(date_str)
socket = cond do
is_nil(socket.assigns.date_from) ->
assign(socket, date_from: date, date_to: nil)
is_nil(socket.assigns.date_to) and Date.compare(date, socket.assigns.date_from) != :lt ->
assign(socket, date_to: date)
true ->
assign(socket, date_from: date, date_to: nil)
end
{:noreply, socket}
end
def handle_event("change-month", %{"dir" => "next"}, socket) do
{:noreply, assign(socket, view_month: Date.shift(socket.assigns.view_month, month: 1))}
end
def handle_event("change-month", %{"dir" => "prev"}, socket) do
{:noreply, assign(socket, view_month: Date.shift(socket.assigns.view_month, month: -1))}
end
"""
use Phoenix.Component
import <%= @module_name %>.ClassMerger, only: [cn: 1]
import <%= @module_name %>.Components.UI.Icon, only: [icon: 1]
attr :id, :string, required: true
attr :view_month, :any, required: true
attr :from, :any, default: nil
attr :to, :any, default: nil
attr :on_change, :string, default: "date-range-changed"
attr :on_month_change, :string, default: "date-range-month"
attr :min_date, :any, default: nil
attr :max_date, :any, default: nil
attr :locale, :string, default: "en"
attr :class, :string, default: nil
attr :rest, :global
def date_range_picker(assigns) do
assigns =
assigns
|> assign(:month1, assigns.view_month)
|> assign(:month2, shift_month(assigns.view_month, 1))
~H"""
<.calendar_month month={@month1} from={@from} to={@to} min_date={@min_date} max_date={@max_date} on_change={@on_change} />
<.calendar_month month={@month2} from={@from} to={@to} min_date={@min_date} max_date={@max_date} on_change={@on_change} />
"""
end
attr :month, :any, required: true
attr :from, :any, default: nil
attr :to, :any, default: nil
attr :min_date, :any, default: nil
attr :max_date, :any, default: nil
attr :on_change, :string, required: true
defp calendar_month(assigns) do
assigns = assign(assigns, :weeks, calendar_weeks(assigns.month))
~H"""
<%%= month_label(@month) %>
<%%= for day_name <- ~w[Su Mo Tu We Th Fr Sa] do %>
| <%%= day_name %> |
<%% end %>
<%%= for week <- @weeks do %>
<%%= for day <- week do %>
<%%= if day do %>
<%% disabled = day_disabled?(day, @min_date, @max_date) %>
<%% is_from = @from && Date.compare(day, @from) == :eq %>
<%% is_to = @to && Date.compare(day, @to) == :eq %>
<%% in_range = in_range?(day, @from, @to) %>
|
|
<%% else %>
|
<%% end %>
<%% end %>
<%% end %>
"""
end
defp shift_month(date, months) do
Date.new!(date.year, date.month, 1)
|> then(fn d ->
total = d.year * 12 + d.month - 1 + months
Date.new!(div(total, 12), rem(total, 12) + 1, 1)
end)
end
defp calendar_weeks(first) do
last = Date.end_of_month(first)
dow = rem(Date.day_of_week(first), 7)
all = List.duplicate(nil, dow) ++ (Date.range(first, last) |> Enum.to_list())
Enum.map(Enum.chunk_every(all, 7), fn w -> w ++ List.duplicate(nil, 7 - length(w)) end)
end
defp in_range?(_day, nil, _to), do: false
defp in_range?(_day, _from, nil), do: false
defp in_range?(day, from, to), do: Date.compare(day, from) != :lt and Date.compare(day, to) != :gt
defp day_disabled?(_day, nil, nil), do: false
defp day_disabled?(day, min, nil), do: Date.compare(day, min) == :lt
defp day_disabled?(day, nil, max), do: Date.compare(day, max) == :gt
defp day_disabled?(day, min, max), do: Date.compare(day, min) == :lt or Date.compare(day, max) == :gt
defp month_label(date) do
names = ~w[January February March April May June July August September October November December]
"#{Enum.at(names, date.month - 1)} #{date.year}"
end
defp format_range(nil, _), do: "Select date range"
defp format_range(from, nil), do: "#{format_date(from)} –"
defp format_range(from, to), do: "#{format_date(from)} – #{format_date(to)}"
defp format_date(date) do
abbrs = ~w[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec]
"#{Enum.at(abbrs, date.month - 1)} #{date.day}, #{date.year}"
end
end