Skip to main content

lib/selecto_components/modal/modal_wrapper.ex

defmodule SelectoComponents.Modal.ModalWrapper do
  @moduledoc """
  Provides a reusable modal wrapper component with animations and backdrop.
  """

  use Phoenix.Component
  alias Phoenix.LiveView.JS

  @doc """
  Modal wrapper component with backdrop and animations.
  """
  def modal(assigns) do
    assigns = assign_new(assigns, :icon, fn -> nil end)
    assigns = assign_new(assigns, :icon_type, fn -> :info end)
    assigns = assign_new(assigns, :show, fn -> true end)
    assigns = assign_new(assigns, :on_prev, fn -> nil end)
    assigns = assign_new(assigns, :on_next, fn -> nil end)

    ~H"""
    <div id={@id} class="relative z-50">
      <%!-- Backdrop --%>
      <div
        id={"#{@id}-bg"}
        class="fixed inset-0 bg-gray-900/35 transition-opacity"
        aria-hidden="true"
        phx-click={@on_cancel}
      />
      
      <%!-- Modal container --%>
      <div
        class="fixed inset-0 z-50 overflow-y-auto"
        aria-labelledby={"#{@id}-title"}
        aria-describedby={"#{@id}-description"}
        role="dialog"
        aria-modal="true"
        tabindex="0"
      >
        <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
          <div
            id={"#{@id}-content"}
            class={"relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 #{size_classes(@size)}"}
          >
            <%!-- Modal header --%>
            <%= if @show_header do %>
              <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
                <div class="sm:flex sm:items-start">
                  <%= if @icon do %>
                    <div class={"mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10 #{icon_bg_class(@icon_type)}"}>
                      <%= render_slot(@icon) %>
                    </div>
                  <% end %>
                  <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
                    <h3 class="text-lg font-medium leading-6 text-gray-900" id={"#{@id}-title"}>
                      <%= @title %>
                    </h3>
                    <%= if @subtitle do %>
                      <div class="mt-2">
                        <p class="text-sm text-gray-500" id={"#{@id}-description"}>
                          <%= @subtitle %>
                        </p>
                      </div>
                    <% end %>
                  </div>
                  <%!-- Close button --%>
                  <button
                    type="button"
                    class="ml-auto bg-white rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                    phx-click={@on_cancel}
                  >
                    <span class="sr-only">Close</span>
                    <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
                      <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                  </button>
                </div>
              </div>
            <% end %>
            
            <%!-- Modal body --%>
            <div class={"#{if @show_header, do: "px-4 pb-4 sm:px-6 sm:pb-4", else: "p-6"} max-h-[70vh] overflow-y-auto"}>
              <%= render_slot(@inner_block) %>
            </div>
            
            <%!-- Modal footer --%>
            <%= if @footer do %>
              <div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
                <%= render_slot(@footer) %>
              </div>
            <% end %>
          </div>
        </div>
      </div>

      <button type="button" class="hidden" phx-window-keydown={@on_cancel} phx-key="escape" />
      <button :if={@on_prev} type="button" class="hidden" phx-window-keydown={@on_prev} phx-key="ArrowLeft" />
      <button :if={@on_next} type="button" class="hidden" phx-window-keydown={@on_next} phx-key="ArrowRight" />
    </div>
    """
  end

  @doc """
  Show modal with animation.
  """
  def show_modal(js \\ %JS{}) do
    js
    |> JS.show(
      to: "#modal",
      transition: {
        "transition-all transform ease-out duration-300",
        "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
        "opacity-100 translate-y-0 sm:scale-100"
      }
    )
    |> JS.show(
      to: "#modal-bg",
      transition: {
        "transition-all transform ease-out duration-300",
        "opacity-0",
        "opacity-100"
      }
    )
    |> JS.show(
      to: "#modal-content",
      transition: {
        "transition-all transform ease-out duration-300",
        "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
        "opacity-100 translate-y-0 sm:scale-100"
      },
      time: 300
    )
    |> JS.focus_first(to: "#modal-content")
  end

  @doc """
  Hide modal with animation.
  """
  def hide_modal(js \\ %JS{}) do
    js
    |> JS.hide(
      to: "#modal-content",
      transition: {
        "transition-all transform ease-in duration-200",
        "opacity-100 translate-y-0 sm:scale-100",
        "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
      }
    )
    |> JS.hide(
      to: "#modal-bg",
      transition: {
        "transition-all transform ease-in duration-200",
        "opacity-100",
        "opacity-0"
      }
    )
    |> JS.hide(to: "#modal", time: 200)
    |> JS.pop_focus()
  end

  # Helper functions

  defp size_classes(:sm), do: "sm:w-full sm:max-w-sm"
  defp size_classes(:md), do: "sm:w-full sm:max-w-lg"
  defp size_classes(:lg), do: "sm:w-full sm:max-w-xl"
  defp size_classes(:xl), do: "sm:w-full sm:max-w-2xl"
  defp size_classes(:full), do: "sm:w-full sm:max-w-4xl"
  defp size_classes(:third), do: "w-full sm:w-[33vw] sm:max-w-[33vw]"
  defp size_classes(:fullscreen), do: "w-[96vw] max-w-[96vw]"
  defp size_classes(_), do: "sm:w-full sm:max-w-lg"

  defp icon_bg_class(:info), do: "bg-blue-100"
  defp icon_bg_class(:success), do: "bg-green-100"
  defp icon_bg_class(:warning), do: "bg-yellow-100"
  defp icon_bg_class(:error), do: "bg-red-100"
  defp icon_bg_class(_), do: "bg-gray-100"
end