Skip to content

Instantly share code, notes, and snippets.

@jmbejar
Last active January 2, 2024 21:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmbejar/f1e21647968e134c91168b489b50c55e to your computer and use it in GitHub Desktop.
Save jmbejar/f1e21647968e134c91168b489b50c55e to your computer and use it in GitHub Desktop.
Calendar Date Picker component
defmodule RetroallyWeb.CalendarDatePickerComponent do
use Phoenix.LiveComponent
@impl true
def mount(socket) do
today = Date.utc_today()
{:ok,
socket
|> assign(:today, today)
|> assign(:visible_month, today.month)
|> assign(:visible_year, today.year)
|> assign(:checked_day, today.day)
|> assign(:selected_date, today)}
end
@impl true
def handle_event("select_day", %{"day" => day}, socket) do
{:noreply, commit_selected_date(socket, String.to_integer(day))}
end
@impl true
def handle_event("next_month", _, socket) do
{:noreply,
socket
|> calculate_next_month()
|> recreate_checked_day()}
end
@impl true
def handle_event("prev_month", _, socket) do
{:noreply,
socket
|> calculate_previous_month()
|> recreate_checked_day()}
end
defp calculate_next_month(socket) do
%{visible_month: month, visible_year: year} = socket.assigns
if month == 12 do
assign(socket, visible_month: 1, visible_year: year + 1)
else
assign(socket, visible_month: month + 1)
end
end
defp calculate_previous_month(socket) do
%{visible_month: month, visible_year: year} = socket.assigns
if month == 1 do
assign(socket, visible_month: 12, visible_year: year - 1)
else
assign(socket, :visible_month, month - 1)
end
end
defp recreate_checked_day(socket) do
%{visible_month: month, visible_year: year, selected_date: selected_date} = socket.assigns
if selected_date.year == year && selected_date.month == month do
commit_selected_date(socket, selected_date.day)
else
assign(socket, :checked_day, nil)
end
end
def commit_selected_date(socket, day) do
socket
|> assign(:checked_day, day)
|> assign(
:selected_date,
Date.new(
socket.assigns.visible_year,
socket.assigns.visible_month,
day
)
|> elem(1)
)
end
def render(assigns) do
~H"""
<div class="flex flex-col items-center justify-center p-2 rounded shadow-card">
<div class="flex items-center justify-between w-full p-3">
<button id="previous_month" class="text-xl" phx-click="prev_month" phx-target={@myself}>
&#60;
</button>
<div class="font-medium text-center uppercase text-primary-900 ">
<%= month_and_year_title(@visible_month, @visible_year) %>
</div>
<button id="next_month" class="text-xl" phx-click="next_month" phx-target={@myself}>
&#62;
</button>
</div>
<div class="grid grid-cols-7 text-center">
<div class="px-1.5 pt-1 pb-5 border-0 text-gray-500">Mon</div>
<div class="px-1.5 pt-1 pb-5 border-0 text-gray-500">Tue</div>
<div class="px-1.5 pt-1 pb-5 border-0 text-gray-500">Wed</div>
<div class="px-1.5 pt-1 pb-5 border-0 text-gray-500">Thu</div>
<div class="px-1.5 pt-1 pb-5 border-0 text-gray-500">Fri</div>
<div class="px-1.5 pt-1 pb-5 border-0 text-gray-500">Sat</div>
<div class="px-1.5 pt-1 pb-5 border-0 text-gray-500">Sun</div>
<%= for {day, month_label, selectable} <-
days_to_display(assigns) do %>
<div class="px-1.5 py-1 text-center">
<%= if selectable do %>
<div
class={css_class_for_day(day, month_label, selectable, assigns)}
phx-click="select_day"
phx-target={@myself}
phx-value-day={day}
>
<%= day %>
</div>
<% else %>
<div class={css_class_for_day(day, month_label, selectable, assigns)}>
<%= day %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
"""
end
defp css_class_for_day(day, month_label, selectable, assigns) do
conditional_class =
cond do
month_label != :current_month ->
"text-gray-300"
assigns.checked_day == day ->
"text-white bg-primary-900 rounded-full cursor-pointer"
selectable ->
"text-primary-900 cursor-pointer"
true ->
"text-gray-500"
end
"h-7 w-7 pt-1 " <> conditional_class
end
defp days_to_display(assigns) do
one_day_in_visible_month = Date.new(assigns.visible_year, assigns.visible_month, 1) |> elem(1)
days_from_previous_month = Date.day_of_week(one_day_in_visible_month) - 1
days_from_next_month =
7 - (one_day_in_visible_month |> Date.end_of_month() |> Date.day_of_week())
previous_month_days =
for day <- days_from_previous_month..1 do
{last_day_of_previous_month(one_day_in_visible_month) - (day - 1), :previous_month}
end
current_month_days =
for day <- 1..Date.days_in_month(one_day_in_visible_month) do
{day, :current_month}
end
next_month_days =
for day <- 1..days_from_next_month do
{day, :next_month}
end
days_to_display = previous_month_days ++ current_month_days ++ next_month_days
Enum.map(days_to_display, fn {day, month_label} ->
case month_label do
:current_month ->
date = Date.new(assigns.visible_year, assigns.visible_month, day) |> elem(1)
{day, month_label, Date.compare(date, assigns.today) != :lt}
month_label ->
{day, month_label, false}
end
end)
end
defp last_day_of_previous_month(date) do
date
|> Date.beginning_of_month()
|> Date.add(-1)
|> Date.days_in_month()
end
defp month_and_year_title(month, year) do
Calendar.strftime(Date.new(year, month, 1) |> elem(1), "%B %Y")
end
end
@andyleclair
Copy link

You have a bug here: https://gist.github.com/jmbejar/f1e21647968e134c91168b489b50c55e#file-calendar_date_picker-ex-L156

If days_from_previous_month is 0 you will do 0..1 which will make bogus days for the previous month. Same bug applies here:

https://gist.github.com/jmbejar/f1e21647968e134c91168b489b50c55e#file-calendar_date_picker-ex-L165

I solved it like this:

    previous_month_days =
      if days_from_previous_month > 0 do
        for day <- days_from_previous_month..1 do
          {last_day_of_previous_month(one_day_in_visible_month) - (day - 1), :previous_month}
        end
      else
        []
      end

Thanks for the component!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment