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 :class, :string, default: nil attr :type, :string, default: "text", values: ~w[text password textarea datetime-local] 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 class={["flex flex-col", @class]}> <.label for={@id}>{@label}</.label> <textarea id={@id} name={@name} class="h-80">{Phoenix.HTML.Form.normalize_value(@type, @value)}</textarea> <.error :for={error <- @errors}>{error}</.error> </div> """ end def input(assigns) do ~H""" <div class="flex flex-col"> <.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 @doc """ Renders markdown content as HTML. ## Examples <.markdown content={@post.body} /> """ attr :content, :string, required: true attr :class, :string, default: "" def markdown(assigns) do ~H""" <div class={["prose", @class]}> {Phoenix.HTML.raw(Web.Markdown.to_html(@content))} </div> """ end @doc """ Renders pagination controls for navigating through a paginated list. ## Examples <.pagination meta={@meta} path={~p"/blog"} schema={Schema.Post} /> """ attr :meta, :map, required: true, doc: "the pagination metadata from Flop" attr :path, :string, required: true, doc: "the base path for pagination links" attr :schema, :atom, required: true, doc: "the schema module for Flop.Phoenix.build_path" attr :class, :string, default: "mt-4", doc: "additional CSS classes" def pagination(assigns) do ~H""" <div class={["flex justify-between", @class]}> <%= if @meta.has_previous_page? do %> <.link navigate={Flop.Phoenix.build_path(@path, Flop.to_previous_cursor(@meta), for: @schema)} class="text-gray-500 hover:text-gray-800" > Newer posts </.link> <% else %> <div></div> <% end %> <%= if @meta.has_next_page? do %> <.link navigate={Flop.Phoenix.build_path(@path, Flop.to_next_cursor(@meta), for: @schema)} class="text-gray-500 hover:text-gray-800" > Older posts </.link> <% else %> <div></div> <% end %> </div> """ end attr :id, :string, required: true attr :stream, Phoenix.LiveView.LiveStream, required: true attr :class, :string, default: nil attr :rest, :global slot :col do attr :label, :string attr :class, :string end def table(assigns) do ~H""" <table id={@id} class={["border-collapse", @class]} {@rest}> <thead> <tr> <%= for col <- @col do %> <th class="border p-2">{col[:label]}</th> <% end %> </tr> </thead> <tbody id={"#{@id}-stream"} phx-update="stream"> <%= for {dom_id, item} <- @stream do %> <tr id={dom_id}> <%= for col <- @col do %> <td class={["border p-2", col[:class]]}>{render_slot(col, item)}</td> <% end %> </tr> <% end %> </tbody> </table> """ end end