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}"}>
-
-
-
-
-
- <.dropdown
- id="calendar-dropdown"
- orientation={:down}
- items={[
- %{name: gettext("Week view"), patch: @current_week_path},
- %{name: gettext("Month view"), patch: @current_month_path}
- ]}
- >
- <:wrapper>
- <.button color={:white} icon="hero-chevron-down-solid" icon_position={:right}>
- <%= if @mode == "month" do %>
- <%= gettext("Month view") %>
- <% else %>
- <%= gettext("Week view") %>
- <% end %>
-
-
-
-
-
-
- <.link patch={@current_week_path}>
-
-
- <.link patch={@current_month_path}>
-
-
-
-
-
-
-
-
-
-
- <.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)}>
- -
-
-
- <%= activity.title %>
-
-
- <.link navigate={Routes.activity_index_path(AtomicWeb.Endpoint, :index)}>
- <.badge variant={:outline} color={:primary} label="Activity" />
-
-
-
-
-
-
- <% 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 %>
- -
- <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)} class="group flex">
-
- <%= activity.title %>
-
-
-
-
- <% 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 idx <- 0..6 do %>
- <% day_of_week = @beginning_of_week |> Timex.add(Duration.from_days(idx)) %>
-
-
- <%= Enum.at(@week, idx) %>
-
- <%= day_of_week |> date_to_day() %>
-
-
-
- <% end %>
-
-
-
-
-
-
-
-
- <%= for hour <- hours() do %>
-
-
- <% end %>
-
-
-
-
-
- <.day date={@current} idx={0} activities={@activities} />
-
-
- <%= for idx <- 0..6 do %>
- <.day date={Timex.shift(@beginning_of_week, days: idx)} idx={idx} activities={@activities} />
- <% end %>
-
-
-
-
-
- """
- 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 %>
+
+
+ -
+ <.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 %>
+
+
+
+
+
+
+
+
+
+ <% 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"""
+
+
+
+ - Enum.take(2)}>
+ <.link patch={Routes.activity_show_path(AtomicWeb.Endpoint, :show, activity)} class="group flex">
+
+ <%= activity.title %>
+
+
+
+
+ - 2} class="text-zinc-500 hover:text-primary-600">
+
+
+
+
+ <.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 idx <- 0..6 do %>
+ <% day_of_week = @beginning_of_week |> Timex.add(Duration.from_days(idx)) %>
+
+
+ <%= Enum.at(@week, idx) %>
+
+ <%= day_of_week |> date_to_day() %>
+
+
+
+ <% end %>
+
+
+
+
+
+
+
+
+ <%= for hour <- hours() do %>
+
+
+ <% end %>
+
+
+
+
+
+ <.day date={@current_date} idx={0} activities={@activities} />
+
+
+ <%= for idx <- 0..6 do %>
+ <.day date={Timex.shift(@beginning_of_week, days: idx)} idx={idx} activities={@activities} />
+ <% end %>
+
+
+
+
+
+ """
+ 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