diff --git a/lib/atomic_web/components/calendar/calendar.ex b/lib/atomic_web/components/calendar/calendar.ex deleted file mode 100644 index 3f1c3cd49..000000000 --- a/lib/atomic_web/components/calendar/calendar.ex +++ /dev/null @@ -1,383 +0,0 @@ -defmodule AtomicWeb.Components.Calendar do - @moduledoc false - use AtomicWeb, :component - - import AtomicWeb.CalendarUtils - import AtomicWeb.Components.CalendarMonth - import AtomicWeb.Components.CalendarWeek - import AtomicWeb.Components.Dropdown - - alias Timex.Duration - - attr :id, :string, default: "calendar", required: false - attr :current_path, :string, required: true - attr :activities, :list, required: true - attr :mode, :string, required: true - attr :timezone, :string, required: true - attr :params, :map, required: true - - def calendar( - %{ - current_path: current_path, - params: params, - mode: mode, - timezone: timezone - } = assigns - ) do - assigns = - assigns - |> assign_date(current_path, params, timezone) - - assigns = - case mode do - "week" -> - assigns - |> assigns_week(current_path, timezone, params) - - "month" -> - assigns - |> assigns_month(current_path, timezone, params) - - _ -> - assigns - |> assigns_month(current_path, timezone, params) - end - - ~H""" -
-
-
-
- - <%= if @mode == "month" do %> - - <% else %> - <%= case date_to_month(@beginning_of_week) == date_to_month(@end_of_week) do %> - <% true -> %> - - <% _ -> %> - <%= if date_to_year(@beginning_of_week) == date_to_year(@end_of_week) do %> - - <% else %> - - <% end %> - <% end %> - <% end %> - -
-
- <.link patch={"#{if @mode == "month" do @previous_month_path else @previous_week_path end}"}> - - - <.link patch={"#{if @mode == "month" do @present_month_path else @present_week_path end}"}> - - - <.link patch={"#{if @mode == "month" do @next_month_path else @next_week_path end}"}> - - -
- -
- -
-
- <.link patch={"#{if @mode == "month" do @present_month_path else @present_week_path end}"}> - - -
- <.link patch={@present_week_path}> - - - <.link patch={@present_month_path}> - - -
-
-
-
-
-
-
- <%= if @mode == "month" do %> - <.calendar_month current_path={@current_path} params={@params} activities={@activities} beginning_of_month={@beginning_of_month} end_of_month={@end_of_month} timezone={@timezone} /> - <% else %> - <.calendar_week current_path={@current_path} current={@current} params={@params} activities={@activities} beginning_of_week={@beginning_of_week} end_of_week={@end_of_week} timezone={@timezone} /> - <% end %> -
- """ - end - - defp assign_date(assigns, current_path, params, timezone) do - current = current_from_params(timezone, params) - - current_year = - current - |> date_to_year() - - current_month = - current - |> date_to_month() - - current_day = - current - |> date_to_day() - - present_year = - Timex.today(timezone) - |> date_to_year() - - present_month = - Timex.today(timezone) - |> date_to_month() - - present_day = - Timex.today(timezone) - |> date_to_day() - - present_week_path = - build_path(current_path, %{ - mode: "week", - day: present_day, - month: present_month, - year: present_year - }) - - current_week_path = - build_path(current_path, %{ - mode: "week", - day: current_day, - month: current_month, - year: current_year - }) - - present_month_path = - build_path(current_path, %{ - mode: "month", - day: present_day, - month: present_month, - year: present_year - }) - - current_month_path = - build_path(current_path, %{ - mode: "month", - day: current_day, - month: current_month, - year: current_year - }) - - assigns - |> assign(present_month_path: present_month_path) - |> assign(present_week_path: present_week_path) - |> assign(current_month_path: current_month_path) - |> assign(current_week_path: current_week_path) - |> assign(current: current) - end - - defp assigns_week(assigns, current_path, timezone, params) do - current = current_from_params(timezone, params) - beginning_of_week = Timex.beginning_of_week(current) - end_of_week = Timex.end_of_week(current) - - previous_week_date = - current - |> Timex.add(Duration.from_days(-7)) - - next_week_date = - current - |> Timex.add(Duration.from_days(7)) - - previous_week_day = - previous_week_date - |> date_to_day() - - previous_week_month = - previous_week_date - |> date_to_month() - - previous_week_year = - previous_week_date - |> date_to_year() - - next_week_day = - next_week_date - |> date_to_day() - - next_week_month = - next_week_date - |> date_to_month() - - next_week_year = - next_week_date - |> date_to_year() - - previous_week_path = - build_path(current_path, %{ - mode: "week", - day: previous_week_day, - month: previous_week_month, - year: previous_week_year - }) - - next_week_path = - build_path(current_path, %{ - mode: "week", - day: next_week_day, - month: next_week_month, - year: next_week_year - }) - - assigns - |> assign(beginning_of_week: beginning_of_week) - |> assign(end_of_week: end_of_week) - |> assign(previous_week_path: previous_week_path) - |> assign(next_week_path: next_week_path) - end - - defp assigns_month(assigns, current_path, timezone, params) do - current = current_from_params(timezone, params) - beginning_of_month = Timex.beginning_of_month(current) - end_of_month = Timex.end_of_month(current) - - last_day_previous_month = - beginning_of_month - |> Timex.add(Duration.from_days(-1)) - - first_day_next_month = - end_of_month - |> Timex.add(Duration.from_days(1)) - - previous_month = - last_day_previous_month - |> date_to_month() - - next_month = - first_day_next_month - |> date_to_month() - - previous_month_year = - last_day_previous_month - |> date_to_year() - - next_month_year = - first_day_next_month - |> date_to_year() - - previous_month_path = - build_path(current_path, %{mode: "month", month: previous_month, year: previous_month_year}) - - next_month_path = - build_path(current_path, %{mode: "month", month: next_month, year: next_month_year}) - - assigns - |> assign(beginning_of_month: beginning_of_month) - |> assign(end_of_month: end_of_month) - |> assign(previous_month_path: previous_month_path) - |> assign(next_month_path: next_month_path) - end -end diff --git a/lib/atomic_web/components/calendar/month.ex b/lib/atomic_web/components/calendar/month.ex deleted file mode 100644 index f19675812..000000000 --- a/lib/atomic_web/components/calendar/month.ex +++ /dev/null @@ -1,152 +0,0 @@ -defmodule AtomicWeb.Components.CalendarMonth do - @moduledoc false - use AtomicWeb, :component - - import AtomicWeb.CalendarUtils - import AtomicWeb.Components.Badge - - attr :id, :string, default: "calendar-month", required: false - attr :current_path, :string, required: true - attr :activities, :list, required: true - attr :timezone, :string, required: true - attr :beginning_of_month, :string, required: true - attr :end_of_month, :string, required: true - attr :params, :map, required: true - - def calendar_month(assigns) do - ~H""" -
-
-
- Mon -
-
- Tue -
-
- Wed -
-
- Thu -
-
- Fri -
-
- Sat -
-
- Sun -
-
-
-
- <%= for i <- 0..@end_of_month.day - 1 do %> - <.day index={i} params={@params} current_path={@current_path} activities={@activities} date={Timex.shift(@beginning_of_month, days: i)} timezone={@timezone} /> - <% end %> -
-
-
-
-
    - <%= for activity <- get_date_activities(@activities, current_from_params(@timezone, @params)) do %> - <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)}> -
  1. -
    -

    - <%= activity.title %> -

    -
    - <.link navigate={Routes.activity_index_path(AtomicWeb.Endpoint, :index)}> - <.badge variant={:outline} color={:primary} label="Activity" /> - - -
    -
    -
  2. - - <% end %> -
-
- """ - end - - defp day(%{index: index, date: date, timezone: timezone} = assigns) do - weekday = Timex.weekday(date, :monday) - today? = Timex.compare(date, Timex.today(timezone)) - - class = - class_list([ - {"relative py-2 px-3 lg:min-h-[110px] lg:flex hidden", true}, - {col_start(weekday), index == 0}, - {"bg-white", today? >= 0}, - {"bg-zinc-50 text-zinc-500", today? < 0} - ]) - - assigns = - assigns - |> assign(disabled: today? < 0) - |> assign(:text, Timex.format!(date, "{D}")) - |> assign(:class, class) - |> assign(:date, date) - |> assign(:today?, today?) - |> assign(:weekday, weekday) - - ~H""" -
- -
    - <%= for activity <- get_date_activities(@activities, @date) do %> -
  1. - <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)} class="group flex"> -

    - <%= activity.title %> -

    - - -
  2. - <% end %> -
-
- <.link patch={build_path(@current_path, %{mode: "month", day: date_to_day(@date), month: date_to_month(@date), year: date_to_year(@date)})} class={"#{if @index == 0 do col_start(@weekday) end} min-h-[56px] flex w-full flex-col bg-white px-3 py-2 text-zinc-900 hover:bg-zinc-100 focus:z-10 lg:hidden"}> - - <%= if (activities = get_date_activities(@activities, @date)) != [] do %> - <%= Enum.count(activities) %> events - - <%= for activity <- activities do %> - <%= if activity do %> - - <% end %> - <% end %> - - <% end %> - - """ - end -end diff --git a/lib/atomic_web/components/calendar/week.ex b/lib/atomic_web/components/calendar/week.ex deleted file mode 100644 index 2362b81eb..000000000 --- a/lib/atomic_web/components/calendar/week.ex +++ /dev/null @@ -1,175 +0,0 @@ -defmodule AtomicWeb.Components.CalendarWeek do - @moduledoc false - use AtomicWeb, :component - - alias Timex.Duration - - import AtomicWeb.CalendarUtils - - attr :id, :string, default: "calendar-week", required: false - attr :current_path, :string, required: true - attr :activities, :list, required: true - attr :timezone, :string, required: true - attr :current, :string, required: true - attr :beginning_of_week, :string, required: true - attr :end_of_week, :string, required: true - attr :params, :map, required: true - - def calendar_week(%{timezone: timezone} = assigns) do - assigns = - assigns - |> assign(week_mobile: ["M", "T", "W", "T", "F", "S", "S"]) - |> assign(week: ["Mon ", "Tue ", "Wed ", "Thu ", "Fri ", "Sat ", "Sun "]) - |> assign(today: Timex.today(timezone)) - - ~H""" -
-
-
-
- <%= for idx <- 0..6 do %> - <% day_of_week = @beginning_of_week |> Timex.add(Duration.from_days(idx)) %> - <.link patch={build_path(@current_path, %{"mode" => "week", "day" => day_of_week |> date_to_day(), "month" => @params["month"], "year" => @params["year"]})} class="flex flex-col items-center py-2"> - <%= Enum.at(@week_mobile, idx) %> - date_to_day() == @params["day"] do - "bg-zinc-900 rounded-full text-white" - else - "text-zinc-900" - end - end} flex items-center justify-center w-8 h-8 mt-1 font-semibold" - }> - <%= day_of_week |> date_to_day() %> - - - <% end %> -
- -
-
-
-
- -
-
- <%= for hour <- hours() do %> -
-
<%= hour %>
-
-
- <% end %> -
- - - -
    - <.day date={@current} idx={0} activities={@activities} /> -
- -
-
-
-
- """ - end - - defp day(assigns) do - ~H""" - <%= for activity <- get_date_activities(@activities, @date) do %> -
  • - <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)}> -
    -

    - <%= activity.title %> -

    -

    - -

    -
    - -
  • - <% end %> - """ - end - - defp calc_row_start(start) do - hours = - start - |> Timex.format!("{h24}") - |> String.to_integer() - - minutes = - start - |> Timex.format!("{m}") - |> String.to_integer() - - minutes = (minutes * 20 / 60) |> trunc() - - 2 + (hours - 8) * 20 + minutes - end - - defp calc_time(start, finish) do - time_diff = (NaiveDateTime.diff(finish, start) / 3600) |> trunc() - - 2 + 20 * time_diff - end - - defp hours, - do: [ - "8H", - "9H", - "10H", - "11H", - "12H", - "13H", - "14H", - "15H", - "16H", - "17H", - "18H", - "19H", - "20H", - "21H", - "22H" - ] -end diff --git a/lib/atomic_web/views/calendar_utils.ex b/lib/atomic_web/live/calendar_live/components/calendar_utils.ex similarity index 68% rename from lib/atomic_web/views/calendar_utils.ex rename to lib/atomic_web/live/calendar_live/components/calendar_utils.ex index f75ea4035..3c56e7c9e 100644 --- a/lib/atomic_web/views/calendar_utils.ex +++ b/lib/atomic_web/live/calendar_live/components/calendar_utils.ex @@ -1,28 +1,24 @@ -defmodule AtomicWeb.CalendarUtils do +defmodule AtomicWeb.CalendarLive.Components.CalendarUtils do @moduledoc """ Calendar utils functions to be used on all views. """ use Phoenix.HTML use Timex - def build_beggining_date(timezone, "month", params) do - current = current_from_params(timezone, params) - Timex.beginning_of_month(current) |> Timex.to_naive_datetime() + def build_beggining_date(_timezone, "month", current_date) do + Timex.beginning_of_month(current_date) |> Timex.to_naive_datetime() end - def build_beggining_date(timezone, "week", params) do - current = current_from_params(timezone, params) - Timex.beginning_of_week(current) |> Timex.to_naive_datetime() + def build_beggining_date(_timezone, "week", current_date) do + Timex.beginning_of_week(current_date) |> Timex.to_naive_datetime() end - def build_ending_date(timezone, "month", params) do - current = current_from_params(timezone, params) - Timex.end_of_month(current) |> Timex.to_naive_datetime() + def build_ending_date(_timezone, "month", current_date) do + Timex.end_of_month(current_date) |> Timex.to_naive_datetime() end - def build_ending_date(timezone, "week", params) do - current = current_from_params(timezone, params) - Timex.end_of_week(current) |> Timex.to_naive_datetime() + def build_ending_date(_timezone, "week", current_date) do + Timex.end_of_week(current_date) |> Timex.to_naive_datetime() end def current_from_params(timezone, %{"day" => day, "month" => month, "year" => year}) do diff --git a/lib/atomic_web/live/calendar_live/components/month.ex b/lib/atomic_web/live/calendar_live/components/month.ex new file mode 100644 index 000000000..7ef4b728f --- /dev/null +++ b/lib/atomic_web/live/calendar_live/components/month.ex @@ -0,0 +1,195 @@ +defmodule AtomicWeb.CalendarLive.Components.CalendarMonth do + @moduledoc false + use AtomicWeb, :component + + import AtomicWeb.CalendarLive.Components.CalendarUtils + + attr :id, :string, default: "calendar-month", required: false + attr :current_date, :string, required: true + attr :activities, :list, required: true + attr :timezone, :string, required: true + attr :beginning_of_month, :string, required: true + attr :end_of_month, :string, required: true + attr :params, :map, required: true + + def calendar_month(assigns) do + ~H""" +
    +
    +
    + Mon +
    +
    + Tue +
    +
    + Wed +
    +
    + Thu +
    +
    + Fri +
    +
    + Sat +
    +
    + Sun +
    +
    +
    +
    + <.day :for={date <- generate_days_list(@beginning_of_month, @end_of_month)} params={@params} date={date} current_date={@current_date} activities={@activities} timezone={@timezone} /> +
    +
    +
    + <%= if length(get_date_activities(@activities, @current_date)) > 0 do %> +
    +
      +
    1. + <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)} class="group flex justify-between p-4 pr-6 focus-within:bg-gray-50 hover:bg-gray-50"> +
      +

      + <%= activity.title %> +

      +
      + +
      +
      + +
    2. +
    +
    + <% end %> + """ + end + + # Generates a list of days for the calendar month, + # including days from the previous and next months to fill the 6x7 grid. + # + # Example + # + # iex> generate_days_list(~D[2024-09-01], ~D[2024-09-30]) + # [ + # ~D[2024-08-26], + # ~D[2024-08-27], + # ~D[2024-08-28], + # ~D[2024-08-29], + # ~D[2024-08-30], + # ~D[2024-08-31], + # ~D[2024-09-01], + # ~D[2024-09-02], + # ..., + # ~D[2024-09-29], + # ~D[2024-09-30], + # ~D[2024-10-01], + # ~D[2024-10-02], + # ~D[2024-10-03], + # ~D[2024-10-04], + # ~D[2024-10-05], + # ~D[2024-10-06] + # ] + + defp generate_days_list(beginning_of_month, end_of_month) do + days_from_last_month = Timex.weekday(beginning_of_month, :monday) + + past_month = + for i <- 1..(days_from_last_month - 1) do + Timex.shift(Timex.shift(beginning_of_month, days: -days_from_last_month), days: i) + end + + current_month = + for i <- 0..(end_of_month.day - 1) do + Timex.shift(beginning_of_month, days: i) + end + + next_month = + for i <- 1..(42 - Timex.days_in_month(beginning_of_month) - days_from_last_month + 1) do + Timex.shift(end_of_month, days: i) + end + + if days_from_last_month == 1 do + current_month ++ next_month + else + past_month ++ current_month ++ next_month + end + end + + defp day(%{date: date, timezone: timezone} = assigns) do + today? = Timex.compare(date, Timex.today(timezone)) + + assigns = + assigns + |> assign(:text, Timex.format!(date, "{D}")) + |> assign(:date, date) + |> assign(:today?, today?) + + ~H""" +
    + +
      +
    1. Enum.take(2)}> + <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)} class="group flex"> +

      + <%= activity.title %> +

      + + +
    2. +
    3. 2} class="text-zinc-500 hover:text-primary-600"> + +
    4. +
    +
    + <.link + phx-click="set-current-date" + phx-value-date={@date} + class={[ + "min-h-[56px] flex w-full flex-col bg-white px-3 py-2 text-zinc-900 focus:z-10 lg:hidden", + @current_date.month == @date.month && "hover:bg-zinc-100", + @current_date.month != @date.month && "bg-zinc-50" + ]} + > + + <%= if (activities = get_date_activities(@activities, @date)) != [] do %> + <%= Enum.count(activities) %> events + + <%= for activity <- Enum.take(activities, 3) do %> + <%= if activity do %> + + <% end %> + <% end %> + + <% end %> + + """ + end +end diff --git a/lib/atomic_web/live/calendar_live/components/week.ex b/lib/atomic_web/live/calendar_live/components/week.ex new file mode 100644 index 000000000..692c59fce --- /dev/null +++ b/lib/atomic_web/live/calendar_live/components/week.ex @@ -0,0 +1,264 @@ +defmodule AtomicWeb.CalendarLive.Components.CalendarWeek do + @moduledoc false + use AtomicWeb, :component + + alias Timex.Duration + + import AtomicWeb.CalendarLive.Components.CalendarUtils + + attr :id, :string, default: "calendar-week", required: false + attr :current_date, :string, required: true + attr :activities, :list, required: true + attr :timezone, :string, required: true + attr :beginning_of_week, :string, required: true + attr :end_of_week, :string, required: true + attr :params, :map, required: true + + def calendar_week(%{timezone: timezone} = assigns) do + days_of_week = + for idx <- 0..6 do + day_of_week = assigns.beginning_of_week |> Timex.add(Duration.from_days(idx)) + day_of_week_mobile = Enum.at(["M", "T", "W", "T", "F", "S", "S"], idx) + {day_of_week, day_of_week_mobile} + end + + assigns = + assigns + |> assign(week: ["Mon ", "Tue ", "Wed ", "Thu ", "Fri ", "Sat ", "Sun "]) + |> assign(today: Timex.today(timezone)) + |> assign(days_of_week: days_of_week) + + ~H""" +
    +
    +
    +
    + <%= for {day_of_week, day_of_week_mobile} <- @days_of_week do %> + <.link phx-click="set-current-date" phx-value-date={day_of_week} class="flex flex-col items-center py-2"> + <%= day_of_week_mobile %> + + <%= day_of_week |> date_to_day() %> + + + <% end %> +
    + +
    +
    +
    +
    + +
    +
    + <%= for hour <- hours() do %> +
    +
    <%= hour %>
    +
    +
    + <% end %> +
    + + + +
      + <.day date={@current_date} idx={0} activities={@activities} /> +
    + +
    +
    +
    +
    + """ + end + + defp day(assigns) do + assigns = + assigns + |> assign_activities_positions(get_date_activities(assigns.activities, assigns.date)) + + ~H""" + <%= for {activity, width, left} <- @activities_with_positions do %> +
  • + <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)}> +
    +

    + <%= activity.title %> +

    +

    + +

    +
    + +
  • + <% end %> + """ + end + + defp assign_activities_positions(assigns, day_activities) do + activities_with_positions = + Enum.map(day_activities, fn activity -> + # Calculate the width of the activity in percentage + width = 1 / (calc_total_overlaps(activity, day_activities) + 1) * 100 + + # Calculate the left position of the activity in percentage + left = + calc_left_amount( + activity, + day_activities, + calc_total_overlaps(activity, day_activities) + 1 + ) + + {activity, width, left} + end) + + assign(assigns, :activities_with_positions, activities_with_positions) + end + + defp calc_row_start(start) do + hours = + start + |> Timex.format!("{h24}") + |> String.to_integer() + + minutes = + start + |> Timex.format!("{m}") + |> String.to_integer() + + hours * 12 + div(minutes, 5) + 2 + end + + # Each row spans a 5-minute interval. + # Calculates the number of grid rows the activity should + # span based on the time difference between the start and finish. + # + # Example + # + # iex> calc_time(~N[2024-09-11 09:00:00], ~N[2024-09-11 10:00:00]) + # 12 + + defp calc_time(start, finish) do + time_diff = (NaiveDateTime.diff(finish, start) / 60) |> trunc() + + if time_diff == 0 do + 1 + else + div(time_diff, 5) + end + end + + # Calculates the total number of overlapping activities with the current activity on the same day. + # + # Example + # + # iex> activities = [ + # ...> %{start: ~N[2024-09-11 09:00:00], finish: ~N[2024-09-11 10:00:00]}, + # ...> %{start: ~N[2024-09-11 09:30:00], finish: ~N[2024-09-11 10:30:00]} + # ...> ] + # ...> + # ...> calc_total_overlaps( + # ...> %{start: ~N[2024-09-11 09:00:00], finish: ~N[2024-09-11 10:00:00]}, + # ...> activities + # ...> ) + # 1 + + def calc_total_overlaps(current_activity, activities) do + current_interval = + Timex.Interval.new(from: current_activity.start, until: current_activity.finish) + + activities + |> Enum.filter(fn activity -> + activity_interval = Timex.Interval.new(from: activity.start, until: activity.finish) + + activity != current_activity && activity.start.day == current_activity.start.day and + Timex.Interval.overlaps?(current_interval, activity_interval) + end) + |> length() + end + + # Calculates the left offset percentage for positioning an activity on the calendar grid. + # + # The left offset is determined by the number of overlapping activities prior to the current one. + # + # + # Example + # + # iex> activities = [ + # ...> %{start: ~N[2024-09-11 09:00:00], finish: ~N[2024-09-11 10:00:00]}, + # ...> %{start: ~N[2024-09-11 09:30:00], finish: ~N[2024-09-11 10:30:00]} + # ...> ] + # ...> + # ...> calc_left_amount( + # ...> %{start: ~N[2024-09-11 09:30:00], finish: ~N[2024-09-11 10:30:00]}, + # ...> activities, + # ...> 2 + # ...> ) + # 50.0 + defp calc_left_amount(current_activity, activities, activity_total_overlaps) do + current_interval = + Timex.Interval.new(from: current_activity.start, until: current_activity.finish) + + # Total number of overlaps prior to the current activity + total_overlaps = + activities + |> Enum.take_while(fn activity -> activity != current_activity end) + |> Enum.filter(fn activity -> + activity_interval = Timex.Interval.new(from: activity.start, until: activity.finish) + + activity != current_activity && activity.start.day == current_activity.start.day and + Timex.Interval.overlaps?(current_interval, activity_interval) + end) + |> length() + + if total_overlaps > 0 do + total_overlaps / activity_total_overlaps * 100 + else + 0 + end + end + + defp hours do + 0..23 |> Enum.map(fn n -> "#{n}H" end) + end +end diff --git a/lib/atomic_web/live/calendar_live/show.ex b/lib/atomic_web/live/calendar_live/show.ex index 706b3dc6e..9167d4bfb 100644 --- a/lib/atomic_web/live/calendar_live/show.ex +++ b/lib/atomic_web/live/calendar_live/show.ex @@ -2,10 +2,14 @@ defmodule AtomicWeb.CalendarLive.Show do @moduledoc false use AtomicWeb, :live_view - import AtomicWeb.CalendarUtils - import AtomicWeb.Components.Calendar + import AtomicWeb.CalendarLive.Components.CalendarUtils + import AtomicWeb.CalendarLive.Components.CalendarMonth + import AtomicWeb.CalendarLive.Components.CalendarWeek + import AtomicWeb.Components.Dropdown alias Atomic.Activities + alias Atomic.Organizations + alias Timex.Duration @impl true def mount(_params, _session, socket) do @@ -14,7 +18,8 @@ defmodule AtomicWeb.CalendarLive.Show do @impl true def handle_params(params, _, socket) do - mode = default_mode(params) + mode = Map.get(params, "mode", "month") + current_date = Map.get(socket.assigns, :current_date, Timex.today(socket.assigns.timezone)) {:noreply, socket @@ -22,17 +27,286 @@ defmodule AtomicWeb.CalendarLive.Show do |> assign(:current_page, :calendar) |> assign(:params, params) |> assign(:mode, mode) - |> assign(:activities, list_activities(socket.assigns.timezone, mode, params))} + |> assign( + :activities, + list_activities(socket.assigns.timezone, mode, current_date, socket.assigns.current_user) + ) + |> assign(:current_path, Routes.calendar_show_path(AtomicWeb.Endpoint, :show)) + |> assign(:current_date, current_date) + |> assigns_month(Routes.calendar_show_path(AtomicWeb.Endpoint, :show)) + |> assigns_week(Routes.calendar_show_path(AtomicWeb.Endpoint, :show))} end - defp list_activities(timezone, mode, params) do - start = build_beggining_date(timezone, mode, params) - finish = build_ending_date(timezone, mode, params) + @impl true + def handle_event("previous", _, socket) do + new_date = + if socket.assigns.mode == "week" do + socket.assigns.current_date |> Timex.add(Duration.from_days(-7)) + else + socket.assigns.beginning_of_month |> Timex.add(Duration.from_days(-1)) + end + + {:noreply, + socket + |> assign( + :current_date, + new_date + ) + |> assign( + :activities, + list_activities( + socket.assigns.timezone, + socket.assigns.mode, + new_date, + socket.assigns.current_user + ) + ) + |> assigns_dates()} + end + + @impl true + def handle_event("present", _, socket) do + new_date = Timex.today(socket.assigns.timezone) + + {:noreply, + socket + |> assign(:current_date, new_date) + |> assign( + :activities, + list_activities( + socket.assigns.timezone, + socket.assigns.mode, + new_date, + socket.assigns.current_user + ) + ) + |> assigns_dates()} + end + + @impl true + def handle_event("next", _, socket) do + new_date = + if socket.assigns.mode == "week" do + socket.assigns.current_date |> Timex.add(Duration.from_days(7)) + else + socket.assigns.end_of_month |> Timex.add(Duration.from_days(1)) + end + + {:noreply, + socket + |> assign(:current_date, new_date) + |> assign( + :activities, + list_activities( + socket.assigns.timezone, + socket.assigns.mode, + new_date, + socket.assigns.current_user + ) + ) + |> assigns_dates()} + end + + @impl true + def handle_event("show-more", %{"date" => date}, socket) do + case Timex.parse(date, "{YYYY}-{0M}-{0D}") do + {:ok, naive_date} -> + date = Timex.to_date(naive_date) + + {:noreply, + socket + |> assign(:current_date, date) + |> push_patch(to: Routes.calendar_show_path(socket, :show, mode: "week"), replace: true)} + + {:error, _reason} -> + {:noreply, socket} + end + end + + @impl true + def handle_event("set-current-date", %{"date" => date}, socket) do + case Timex.parse(date, "{YYYY}-{0M}-{0D}") do + {:ok, naive_date} -> + date = Timex.to_date(naive_date) - Activities.list_activities_from_to(start, finish) + date = + if date.month != socket.assigns.current_date.month && socket.assigns.mode == "month" do + socket.assigns.current_date + else + date + end + + {:noreply, + socket + |> assign(:current_date, date)} + + {:error, _reason} -> + {:noreply, socket} + end + end + + defp assigns_dates(socket) do + if socket.assigns.mode == "week" do + assigns_week(socket, Routes.calendar_show_path(AtomicWeb.Endpoint, :show)) + else + assigns_month(socket, Routes.calendar_show_path(AtomicWeb.Endpoint, :show)) + end end - defp default_mode(params) when is_map_key(params, "mode"), do: params["mode"] + defp assigns_month(socket, current_path) do + current = socket.assigns.current_date + beginning_of_month = Timex.beginning_of_month(current) + end_of_month = Timex.end_of_month(current) + + last_day_previous_month = + beginning_of_month + |> Timex.add(Duration.from_days(-1)) + + first_day_next_month = + end_of_month + |> Timex.add(Duration.from_days(1)) + + previous_month = + last_day_previous_month + |> date_to_month() + + next_month = + first_day_next_month + |> date_to_month() - defp default_mode(_params), do: "month" + previous_month_year = + last_day_previous_month + |> date_to_year() + + next_month_year = + first_day_next_month + |> date_to_year() + + previous_month_path = + build_path(current_path, %{mode: "month", month: previous_month, year: previous_month_year}) + + next_month_path = + build_path(current_path, %{mode: "month", month: next_month, year: next_month_year}) + + socket + |> assign(beginning_of_month: beginning_of_month) + |> assign(end_of_month: end_of_month) + |> assign(previous_month_path: previous_month_path) + |> assign(next_month_path: next_month_path) + end + + defp assigns_week(socket, current_path) do + current = socket.assigns.current_date + beginning_of_week = Timex.beginning_of_week(current) + end_of_week = Timex.end_of_week(current) + + previous_week_date = + current + |> Timex.add(Duration.from_days(-7)) + + next_week_date = + current + |> Timex.add(Duration.from_days(7)) + + previous_week_day = + previous_week_date + |> date_to_day() + + previous_week_month = + previous_week_date + |> date_to_month() + + previous_week_year = + previous_week_date + |> date_to_year() + + next_week_day = + next_week_date + |> date_to_day() + + next_week_month = + next_week_date + |> date_to_month() + + next_week_year = + next_week_date + |> date_to_year() + + previous_week_path = + build_path(current_path, %{ + mode: "week", + day: previous_week_day, + month: previous_week_month, + year: previous_week_year + }) + + next_week_path = + build_path(current_path, %{ + mode: "week", + day: next_week_day, + month: next_week_month, + year: next_week_year + }) + + socket + |> assign(beginning_of_week: beginning_of_week) + |> assign(end_of_week: end_of_week) + |> assign(previous_week_path: previous_week_path) + |> assign(next_week_path: next_week_path) + end + + defp multi_day_activity?(activity) do + Timex.diff(activity.finish, activity.start, :days) > 0 + end + + defp split_activity_by_day(activity) do + start_date = Timex.to_date(activity.start) + end_date = Timex.to_date(activity.finish) + + Enum.map(0..Timex.diff(end_date, start_date, :days), fn offset -> + day_start = + if offset > 0 do + Timex.shift(activity.start, days: offset) |> Timex.beginning_of_day() + else + activity.start + end + + day_end = + if offset == Timex.diff(end_date, start_date, :days) do + activity.finish + else + Timex.end_of_day(day_start) + end + + %{activity | start: day_start, finish: day_end} + end) + end + + defp split_all_activities_by_day(activities) do + Enum.map(activities, fn activity -> + if multi_day_activity?(activity) do + split_activity_by_day(activity) + else + [activity] + end + end) + |> List.flatten() + end + + defp list_activities(timezone, mode, current_date, current_user) do + if current_user do + organizations_id = + Organizations.list_organizations_followed_by_user(current_user.id) + |> Enum.map(fn org -> org.id end) + + start = Timex.shift(build_beggining_date(timezone, mode, current_date), days: -7) + finish = Timex.shift(build_ending_date(timezone, mode, current_date), days: 7) + + Activities.list_activities_from_to(start, finish) + |> Enum.filter(fn activity -> activity.organization_id in organizations_id end) + |> split_all_activities_by_day() + else + [] + end + end end diff --git a/lib/atomic_web/live/calendar_live/show.html.heex b/lib/atomic_web/live/calendar_live/show.html.heex index 4af43197f..8b706aaea 100644 --- a/lib/atomic_web/live/calendar_live/show.html.heex +++ b/lib/atomic_web/live/calendar_live/show.html.heex @@ -1,3 +1,83 @@ <.page title="Calendar"> - <.calendar current_path={Routes.calendar_show_path(@socket, :show)} activities={@activities} params={@params} mode={@mode} timezone={@timezone} /> +
    +
    +
    +
    +
    + + <%= if @mode == "month" do %> + + <% else %> + <%= if date_to_month(@beginning_of_week) == date_to_month(@end_of_week) do %> + + <% else %> + <%= if date_to_year(@beginning_of_week) == date_to_year(@end_of_week) do %> + + <% else %> + + <% end %> + <% end %> + <% end %> + +
    +
    + + + + +
    +
    + <.dropdown + id="calendar-dropdown" + orientation={:down} + items={ + [ + %{name: gettext("Go to today"), phx_click: "present", class: "md:hidden border-b border-zinc-100"}, + # Link that switches mode + %{name: gettext("Week view"), patch: "?mode=week"}, + %{name: gettext("Month view"), patch: "?mode=month"} + ] + } + > + <:wrapper> + + + + +
    +
    +
    +
    +
    + <%= if @mode == "month" do %> + <.calendar_month current_date={@current_date} params={@params} activities={@activities} beginning_of_month={@beginning_of_month} end_of_month={@end_of_month} timezone={@timezone} /> + <% else %> + <.calendar_week current_date={@current_date} params={@params} activities={@activities} beginning_of_week={@beginning_of_week} end_of_week={@end_of_week} timezone={@timezone} /> + <% end %> +
    +
    +
    + <.icon name="hero-information-circle" class="size-5" /> +

    <%= gettext("See here the activities of the organizations you follow.") %>

    +
    diff --git a/storybook/components/calendar.story.exs b/storybook/components/calendar.story.exs deleted file mode 100644 index f4e6679ed..000000000 --- a/storybook/components/calendar.story.exs +++ /dev/null @@ -1,32 +0,0 @@ -defmodule AtomicWeb.Storybook.Components.Calendar do - use PhoenixStorybook.Story, :component - - alias AtomicWeb.Components.Calendar - - def function, do: &Calendar.calendar/1 - - def variations do - [ - %Variation{ - id: :default, - attributes: %{ - current_path: "/storybook/components/calendar", - activities: [], - mode: "month", - timezone: "Europe/Lisbon", - params: %{} - } - }, - %Variation{ - id: :weekly, - attributes: %{ - current_path: "/storybook/components/calendar", - activities: [], - mode: "week", - timezone: "Europe/Lisbon", - params: %{} - } - } - ] - end -end