183 lines
6 KiB
Elixir
183 lines
6 KiB
Elixir
|
defmodule GroupdleWeb.CoreComponents do
|
|||
|
@moduledoc """
|
|||
|
Provides core UI components.
|
|||
|
|
|||
|
At first glance, this module may seem daunting, but its goal is to provide
|
|||
|
core building blocks for your application, such as modals, tables, and
|
|||
|
forms. The components consist mostly of markup and are well-documented
|
|||
|
with doc strings and declarative assigns. You may customize and style
|
|||
|
them in any way you want, based on your application growth and needs.
|
|||
|
|
|||
|
The default components use Tailwind CSS, a utility-first CSS framework.
|
|||
|
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
|
|||
|
how to customize them or feel free to swap in another framework altogether.
|
|||
|
|
|||
|
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
|||
|
"""
|
|||
|
use Phoenix.Component
|
|||
|
|
|||
|
alias Phoenix.LiveView.JS
|
|||
|
|
|||
|
@doc """
|
|||
|
Renders flash notices.
|
|||
|
|
|||
|
## Examples
|
|||
|
|
|||
|
<.flash kind={:info} flash={@flash} />
|
|||
|
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
|
|||
|
"""
|
|||
|
attr :id, :string, doc: "the optional id of flash container"
|
|||
|
attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
|
|||
|
attr :title, :string, default: nil
|
|||
|
attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
|
|||
|
attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
|
|||
|
|
|||
|
slot :inner_block, doc: "the optional inner block that renders the flash message"
|
|||
|
|
|||
|
def flash(assigns) do
|
|||
|
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
|
|||
|
|
|||
|
~H"""
|
|||
|
<div
|
|||
|
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
|
|||
|
id={@id}
|
|||
|
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
|
|||
|
role="alert"
|
|||
|
class={[
|
|||
|
"fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
|
|||
|
@kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
|
|||
|
@kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
|
|||
|
]}
|
|||
|
{@rest}
|
|||
|
>
|
|||
|
<p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
|
|||
|
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
|
|||
|
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
|
|||
|
{@title}
|
|||
|
</p>
|
|||
|
<p class="mt-2 text-sm leading-5">{msg}</p>
|
|||
|
<button type="button" class="group absolute top-1 right-1 p-2" aria-label="close">
|
|||
|
<.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
|
|||
|
</button>
|
|||
|
</div>
|
|||
|
"""
|
|||
|
end
|
|||
|
|
|||
|
@doc """
|
|||
|
Shows the flash group with standard titles and content.
|
|||
|
|
|||
|
## Examples
|
|||
|
|
|||
|
<.flash_group flash={@flash} />
|
|||
|
"""
|
|||
|
attr :flash, :map, required: true, doc: "the map of flash messages"
|
|||
|
attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
|
|||
|
|
|||
|
def flash_group(assigns) do
|
|||
|
~H"""
|
|||
|
<div id={@id}>
|
|||
|
<.flash kind={:info} title="Success!" flash={@flash} />
|
|||
|
<.flash kind={:error} title="Error!" flash={@flash} />
|
|||
|
<.flash
|
|||
|
id="client-error"
|
|||
|
kind={:error}
|
|||
|
title="We can't find the internet"
|
|||
|
phx-disconnected={show(".phx-client-error #client-error")}
|
|||
|
phx-connected={hide("#client-error")}
|
|||
|
hidden
|
|||
|
>
|
|||
|
Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
|||
|
</.flash>
|
|||
|
|
|||
|
<.flash
|
|||
|
id="server-error"
|
|||
|
kind={:error}
|
|||
|
title="Something went wrong!"
|
|||
|
phx-disconnected={show(".phx-server-error #server-error")}
|
|||
|
phx-connected={hide("#server-error")}
|
|||
|
hidden
|
|||
|
>
|
|||
|
Hang in there while we get back on track
|
|||
|
<.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
|
|||
|
</.flash>
|
|||
|
</div>
|
|||
|
"""
|
|||
|
end
|
|||
|
|
|||
|
@doc """
|
|||
|
Renders a [Heroicon](https://heroicons.com).
|
|||
|
|
|||
|
Heroicons come in three styles – outline, solid, and mini.
|
|||
|
By default, the outline style is used, but solid and mini may
|
|||
|
be applied by using the `-solid` and `-mini` suffix.
|
|||
|
|
|||
|
You can customize the size and colors of the icons by setting
|
|||
|
width, height, and background color classes.
|
|||
|
|
|||
|
Icons are extracted from the `deps/heroicons` directory and bundled within
|
|||
|
your compiled app.css by the plugin in your `assets/tailwind.config.js`.
|
|||
|
|
|||
|
## Examples
|
|||
|
|
|||
|
<.icon name="hero-x-mark-solid" />
|
|||
|
<.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
|
|||
|
"""
|
|||
|
attr :name, :string, required: true
|
|||
|
attr :class, :string, default: nil
|
|||
|
|
|||
|
def icon(%{name: "hero-" <> _} = assigns) do
|
|||
|
~H"""
|
|||
|
<span class={[@name, @class]} />
|
|||
|
"""
|
|||
|
end
|
|||
|
|
|||
|
## JS Commands
|
|||
|
|
|||
|
def show(js \\ %JS{}, selector) do
|
|||
|
JS.show(js,
|
|||
|
to: selector,
|
|||
|
time: 300,
|
|||
|
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"}
|
|||
|
)
|
|||
|
end
|
|||
|
|
|||
|
def hide(js \\ %JS{}, selector) do
|
|||
|
JS.hide(js,
|
|||
|
to: selector,
|
|||
|
time: 200,
|
|||
|
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"}
|
|||
|
)
|
|||
|
end
|
|||
|
|
|||
|
@doc """
|
|||
|
Translates an error message using gettext.
|
|||
|
"""
|
|||
|
def translate_error({msg, opts}) do
|
|||
|
# You can make use of gettext to translate error messages by
|
|||
|
# uncommenting and adjusting the following code:
|
|||
|
|
|||
|
# if count = opts[:count] do
|
|||
|
# Gettext.dngettext(GroupdleWeb.Gettext, "errors", msg, msg, count, opts)
|
|||
|
# else
|
|||
|
# Gettext.dgettext(GroupdleWeb.Gettext, "errors", msg, opts)
|
|||
|
# end
|
|||
|
|
|||
|
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
|||
|
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
|||
|
end)
|
|||
|
end
|
|||
|
|
|||
|
@doc """
|
|||
|
Translates the errors for a field from a keyword list of errors.
|
|||
|
"""
|
|||
|
def translate_errors(errors, field) when is_list(errors) do
|
|||
|
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
|||
|
end
|
|||
|
end
|