182 lines
6 KiB
Elixir
182 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
|