lib/pgflow_dashboard/components/metric_card.ex

defmodule PgFlowDashboard.Components.MetricCard do
  @moduledoc """
  Dashboard metric card component.
  """

  use Phoenix.Component

  @doc """
  Renders a metric card.

  ## Attributes

    * `:title` - The metric title
    * `:value` - The metric value
    * `:subtitle` - Optional subtitle text
    * `:trend` - Optional trend indicator (:up, :down, :neutral)
    * `:trend_value` - Optional trend percentage/value

  """
  attr(:title, :string, required: true)
  attr(:value, :any, required: true)
  attr(:subtitle, :string, default: nil)
  attr(:trend, :atom, default: nil)
  attr(:trend_value, :string, default: nil)
  attr(:class, :string, default: nil)
  attr(:href, :string, default: nil)

  def metric_card(assigns) do
    ~H"""
    <.card_wrapper href={@href} class={@class}>
      <div class="flex items-start justify-between">
        <div>
          <p class="text-sm font-medium text-slate-500 dark:text-slate-400">{@title}</p>
          <p class="mt-1 text-2xl font-semibold text-slate-900 dark:text-white">{format_value(@value)}</p>
          <p :if={@subtitle} class="mt-1 text-xs text-slate-500 dark:text-slate-400">{@subtitle}</p>
        </div>

        <div :if={@trend && @trend_value} class={[
          "flex items-center gap-0.5 text-sm font-medium",
          @trend == :up && "text-emerald-600 dark:text-emerald-400",
          @trend == :down && "text-rose-600 dark:text-rose-400",
          @trend == :neutral && "text-slate-500 dark:text-slate-400"
        ]}>
          <.trend_icon trend={@trend} />
          {@trend_value}
        </div>
      </div>
    </.card_wrapper>
    """
  end

  attr(:href, :string, default: nil)
  attr(:class, :string, default: nil)
  slot(:inner_block, required: true)

  defp card_wrapper(%{href: nil} = assigns) do
    ~H"""
    <div class={[
      "bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-4",
      @class
    ]}>
      {render_slot(@inner_block)}
    </div>
    """
  end

  defp card_wrapper(assigns) do
    ~H"""
    <.link
      navigate={@href}
      class={[
        "block bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-4 transition-colors cursor-pointer hover:border-purple-300 dark:hover:border-purple-600",
        @class
      ]}
    >
      {render_slot(@inner_block)}
    </.link>
    """
  end

  defp trend_icon(%{trend: :up} = assigns) do
    ~H"""
    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
    </svg>
    """
  end

  defp trend_icon(%{trend: :down} = assigns) do
    ~H"""
    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
    </svg>
    """
  end

  defp trend_icon(assigns) do
    ~H"""
    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14" />
    </svg>
    """
  end

  defp format_value(value) when is_integer(value),
    do: Number.Delimit.number_to_delimited(value, precision: 0)

  defp format_value(value) when is_float(value),
    do: Number.Delimit.number_to_delimited(value, precision: 1)

  defp format_value(value), do: value
end

# Simple number formatting module
defmodule Number.Delimit do
  @moduledoc false

  @doc false
  def number_to_delimited(number, opts \\ []) do
    precision = Keyword.get(opts, :precision, 2)
    delimiter = Keyword.get(opts, :delimiter, ",")

    number
    |> format_number(precision)
    |> add_delimiter(delimiter)
  end

  defp format_number(number, 0) when is_integer(number), do: Integer.to_string(number)

  defp format_number(number, 0) when is_float(number),
    do: number |> round() |> Integer.to_string()

  defp format_number(number, precision) when is_float(number),
    do: :erlang.float_to_binary(number, decimals: precision)

  defp format_number(number, _precision), do: to_string(number)

  defp add_delimiter(str, delimiter) do
    case String.split(str, ".") do
      [int_part] -> delimit_integer(int_part, delimiter)
      [int_part, dec_part] -> delimit_integer(int_part, delimiter) <> "." <> dec_part
    end
  end

  defp delimit_integer(str, delimiter) do
    str
    |> String.graphemes()
    |> Enum.reverse()
    |> Enum.chunk_every(3)
    |> Enum.join(delimiter)
    |> String.reverse()
  end
end