defmodule PhoenixKit.Modules.Publishing.Web.Components.VersionSwitcher do
@moduledoc """
Version switcher component for publishing posts.
Displays available versions as a compact inline list with status indicators.
Used in the publishing editor to navigate between different versions of a post.
## Display Format
Admin mode: v1 (●) | v2 (●) | v3 (○)
- Green dot (●): Published (this IS the live version)
- Yellow dot (●): Draft
- Gray dot (●): Archived
- Current version is highlighted
## Examples
# Editor: switch between versions
<.publishing_version_switcher
versions={@available_versions}
version_statuses={@version_statuses}
current_version={@current_version}
on_click="switch_version"
/>
"""
use Phoenix.Component
use Gettext, backend: PhoenixKitWeb.Gettext
@doc """
Renders a compact inline version switcher.
## Attributes
- `versions` - List of version numbers (integers)
- `version_statuses` - Map of version number => status string ("published", "draft", "archived")
- `version_dates` - Map of version number => ISO 8601 date string (shown in tooltip)
- `current_version` - Currently active version number
- `on_click` - Event name for click handler
- `phx_target` - Target for the event
- `class` - Additional CSS classes
- `size` - Size variant: :xs, :sm, :md (default: :sm)
Note: Only ONE version can have status "published" at a time. This is the live version.
"""
attr :versions, :list, required: true
attr :version_statuses, :map, default: %{}
attr :version_dates, :map, default: %{}
attr :current_version, :integer, default: nil
attr :on_click, :string, default: nil
attr :phx_target, :any, default: nil
attr :class, :string, default: ""
attr :size, :atom, default: :sm, values: [:xs, :sm, :md]
def publishing_version_switcher(assigns) do
# Sort versions in ascending order
sorted_versions = Enum.sort(assigns.versions)
# Determine which version is actually "live" (highest published version)
# Only ONE version should show as live - the one users will see publicly
live_version =
sorted_versions
|> Enum.filter(fn v -> Map.get(assigns.version_statuses, v) == "published" end)
|> Enum.max(fn -> nil end)
assigns =
assigns
|> assign(:sorted_versions, sorted_versions)
|> assign(:live_version, live_version)
~H"""
<%= for {version, index} <- Enum.with_index(@sorted_versions) do %>
<%= if index > 0 do %>
|
<% end %>
<.version_item
version={version}
status={Map.get(@version_statuses, version)}
date={Map.get(@version_dates, version)}
is_current={version == @current_version}
is_live={version == @live_version}
on_click={@on_click}
phx_target={@phx_target}
size={@size}
/>
<% end %>
"""
end
attr :version, :integer, required: true
attr :status, :string, default: nil
attr :date, :string, default: nil
attr :is_current, :boolean, default: false
attr :is_live, :boolean, default: false
attr :on_click, :string, default: nil
attr :phx_target, :any, default: nil
attr :size, :atom, default: :sm
defp version_item(assigns) do
~H"""
<%= if @on_click do %>
<% else %>
<.version_content
version={@version}
status={@status}
is_current={@is_current}
is_live={@is_live}
size={@size}
/>
<% end %>
"""
end
attr :version, :integer, required: true
attr :status, :string, default: nil
attr :is_current, :boolean, default: false
attr :is_live, :boolean, default: false
attr :size, :atom, default: :sm
defp version_content(assigns) do
~H"""
v{@version}
<%= if @is_live do %>
(live)
<% end %>
"""
end
# Status dot styling - live = green, published (not live) = blue, draft = yellow, archived = gray
defp status_dot_classes(status, is_live, size) do
base = ["rounded-full", "inline-block", dot_size_class(size)]
color =
cond do
is_live -> "bg-success"
status == "published" -> "bg-info"
status == "draft" -> "bg-warning"
status == "archived" -> "bg-base-content/40"
true -> "bg-base-content/20"
end
base ++ [color]
end
# Item container classes
defp item_classes(is_current, size) do
base = [
"inline-flex items-center rounded transition-colors",
size_padding_class(size)
]
state =
if is_current do
"bg-primary/10 text-primary font-semibold"
else
"hover:bg-base-200 cursor-pointer"
end
base ++ [state]
end
# Code text classes
defp code_classes(is_current, size) do
base = [size_text_class(size)]
weight = if is_current, do: "font-semibold", else: "font-medium"
base ++ [weight]
end
# Size-based classes
defp dot_size_class(:xs), do: "w-1.5 h-1.5"
defp dot_size_class(:sm), do: "w-2 h-2"
defp dot_size_class(:md), do: "w-2.5 h-2.5"
defp size_text_class(:xs), do: "text-xs"
defp size_text_class(:sm), do: "text-sm"
defp size_text_class(:md), do: "text-base"
defp size_padding_class(:xs), do: "px-1 py-0.5"
defp size_padding_class(:sm), do: "px-1.5 py-0.5"
defp size_padding_class(:md), do: "px-2 py-1"
defp size_gap_class(:xs), do: "gap-0.5"
defp size_gap_class(:sm), do: "gap-1"
defp size_gap_class(:md), do: "gap-1.5"
defp size_separator_class(:xs), do: "text-xs"
defp size_separator_class(:sm), do: "text-sm"
defp size_separator_class(:md), do: "text-base"
# Generate title/tooltip text
defp version_title(version, status, is_live, date) do
status_text =
cond do
is_live -> gettext("Published (Live)")
status == "published" -> gettext("Published")
status == "draft" -> gettext("Draft")
status == "archived" -> gettext("Archived")
true -> gettext("Unknown")
end
base = gettext("Version %{version} - %{status}", version: version, status: status_text)
case format_version_date(date) do
nil -> base
formatted_date -> "#{base}\n#{gettext("Created:")} #{formatted_date}"
end
end
# Format ISO 8601 date string for display
defp format_version_date(nil), do: nil
defp format_version_date(""), do: nil
defp format_version_date(date_string) when is_binary(date_string) do
case DateTime.from_iso8601(date_string) do
{:ok, datetime, _offset} ->
Calendar.strftime(datetime, "%Y-%m-%d %H:%M")
_ ->
# Try parsing as NaiveDateTime
case NaiveDateTime.from_iso8601(date_string) do
{:ok, naive_dt} -> Calendar.strftime(naive_dt, "%Y-%m-%d %H:%M")
_ -> nil
end
end
end
defp format_version_date(_), do: nil
end