defmodule Web.CoreComponents do @moduledoc """ Provides core UI components. """ use Phoenix.Component alias Phoenix.HTML.FormField attr :id, :any, default: nil attr :name, :any attr :label, :string, default: nil attr :value, :any attr :type, :string, default: "text", values: ~w[text password textarea] attr :field, FormField attr :errors, :list, default: [] attr :rest, :global, include: ~w[disabled form pattern placeholder readonly required] def input(%{field: %FormField{} = field} = assigns) do errors = if Phoenix.Component.used_input?(field) do field.errors else [] end assigns |> assign( field: nil, id: assigns.id || field.id, errors: Enum.map(errors, &translate_error/1) ) |> assign_new(:name, fn -> field.name end) |> assign_new(:value, fn -> field.value end) |> input() end def input(%{type: "textarea"} = assigns) do ~H""" <div> <.label for={@id}>{@label}</.label> <textarea id={@id} name={@name}>{Phoenix.HTML.Form.normalize_value(@type, @value)}</textarea> <.error :for={error <- @errors}>{error}</.error> </div> """ end def input(assigns) do ~H""" <div> <.label for={@id}>{@label}</.label> <input id={@id} type={@type} name={@name} value={Phoenix.HTML.Form.normalize_value(@type, @value)} {@rest} /> <.error :for={error <- @errors}>{error}</.error> </div> """ end attr :for, :string, default: nil slot :inner_block, required: true def label(assigns) do ~H""" <label for={@for}> {render_slot(@inner_block)} </label> """ end slot :inner_block, required: true def error(assigns) do ~H""" <p> <.icon name="hero-exclamation-circle-mini" class="h-5 w-5 flex-none" /> {render_slot(@inner_block)} </p> """ end attr :name, :string, required: true attr :class, :string, default: nil def icon(%{name: "hero-" <> _} = assigns) do ~H""" <span class={[@name, @class]} /> """ end def translate_error({msg, opts}) do Enum.reduce(opts, msg, fn {key, value}, acc -> String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end) end) end def translate_errors(errors, field) when is_list(errors) do for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts}) end attr :type, :string, default: "button", values: ~w[button submit] attr :rest, :global slot :inner_block, required: true def button(assigns) do ~H""" <button type={@type} {@rest}> {render_slot(@inner_block)} </button> """ end attr :format, :string, required: true attr :value, :any, default: nil attr :formatter, :atom, default: :default attr :timezone, :string, default: "America/New_York" attr :global, :global def timex(%{value: nil} = assigns) do ~H""" <time datetime="">--</time> """ end def timex(%{value: value, timezone: timezone} = assigns) do assigns = assign_new(assigns, :local_value, fn -> case value do %DateTime{} = datetime -> datetime %NaiveDateTime{} = naive -> naive |> DateTime.from_naive!("Etc/UTC") |> DateTime.shift_zone!(timezone) end end) ~H""" <time datetime={Timex.format!(@local_value, "{ISO:Extended}")} title={Timex.format!(@local_value, "{Mshort} {D}, {YYYY}, {h12}:{m} {AM} {Zabbr}")} {@global} > {Timex.format!(@local_value, @format, timex_formatter(@formatter))} </time> """ end defp timex_formatter(formatter) do Module.concat(Timex.Format.DateTime.Formatters, :string.titlecase("#{formatter}")) end attr :id, :string, required: true attr :posts, :any, required: true slot :inner_block, required: true def post_list(assigns) do ~H""" <ol id={@id} phx-update={if is_struct(@posts, Phoenix.LiveView.LiveStream), do: "phx-update"}> <li :for={{dom_id, item} <- normalize_posts(@posts)} id={dom_id} class="flex flex-row justify-between" > <span>{render_slot(@inner_block, item)}</span> <span> <%= if item.deleted_at do %> deleted <% else %> <%= if item.published_at do %> <%= case item.kind do %> <% :blog -> %> <.timex value={item.published_at} format="{YYYY}-{0M}-{0D}" /> <% :status -> %> <.timex value={item.published_at} format="{relative}" formatter={:relative} /> <% end %> <% else %> draft <% end %> <% end %> </span> </li> </ol> """ end defp normalize_posts(%Phoenix.LiveView.LiveStream{} = stream), do: stream defp normalize_posts(posts) when is_list(posts), do: Enum.with_index(posts, &{"#{&1.kind}-#{&2}", &1}) end