chiya/lib/chiya_web/components/core_components.ex

834 lines
27 KiB
Elixir
Raw Normal View History

2023-03-05 16:07:40 +01:00
defmodule ChiyaWeb.CoreComponents do
@moduledoc """
Provides core UI components.
The components in this module use Tailwind CSS, a utility-first CSS framework.
See the [Tailwind CSS documentation](https://tailwindcss.com) to learn how to
customize the generated components in this module.
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
"""
use Phoenix.Component
2023-03-31 16:58:10 +02:00
use Phoenix.VerifiedRoutes,
endpoint: ChiyaWeb.Endpoint,
router: ChiyaWeb.Router,
statics: ChiyaWeb.static_paths()
2023-03-05 16:07:40 +01:00
alias Phoenix.LiveView.JS
import ChiyaWeb.Gettext
2023-03-23 08:24:13 +01:00
def favicon(assigns) do
~H"""
2023-03-31 16:58:10 +02:00
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link rel="manifest" href="/favicon/site.webmanifest" />
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
"""
end
def darkmode_toggle(assigns) do
~H"""
<.link href="#" id="dark-mode-toggle" class="text-sm leading-6">
2023-06-02 07:31:47 +02:00
<span class="hidden dark:inline">🌙</span>
<span class="inline dark:hidden"></span>
</.link>
"""
end
2023-03-05 16:07:40 +01:00
@doc """
Renders a modal.
## Examples
<.modal id="confirm-modal">
Are you sure?
<:confirm>OK</:confirm>
<:cancel>Cancel</:cancel>
</.modal>
JS commands may be passed to the `:on_cancel` and `on_confirm` attributes
for the caller to react to each button press, for example:
<.modal id="confirm" on_confirm={JS.push("delete")} on_cancel={JS.navigate(~p"/posts")}>
Are you sure you?
<:confirm>OK</:confirm>
<:cancel>Cancel</:cancel>
</.modal>
"""
attr :id, :string, required: true
attr :show, :boolean, default: false
attr :on_cancel, JS, default: %JS{}
attr :on_confirm, JS, default: %JS{}
slot :inner_block, required: true
slot :title
slot :subtitle
slot :confirm
slot :cancel
def modal(assigns) do
~H"""
<div
id={@id}
phx-mounted={@show && show_modal(@id)}
phx-remove={hide_modal(@id)}
class="relative z-50 hidden"
>
<div
id={"#{@id}-bg"}
class="fixed inset-0 bg-gray-50/90 dark:bg-gray-900/90 transition-opacity"
aria-hidden="true"
/>
2023-03-05 16:07:40 +01:00
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-describedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center">
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
<.focus_wrap
id={"#{@id}-container"}
phx-mounted={@show && show_modal(@id)}
phx-window-keydown={hide_modal(@on_cancel, @id)}
phx-key="escape"
phx-click-away={hide_modal(@on_cancel, @id)}
class="hidden relative rounded-2xl bg-white p-14 shadow-lg shadow-gray-700/10 ring-1 ring-gray-700/10 transition dark:bg-gray-900"
2023-03-05 16:07:40 +01:00
>
<div class="absolute top-6 right-5">
<button
phx-click={hide_modal(@on_cancel, @id)}
type="button"
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark-solid" class="w-5 h-5 dark:text-gray-50" />
2023-03-05 16:07:40 +01:00
</button>
</div>
<div id={"#{@id}-content"}>
<header :if={@title != []}>
<h1 id={"#{@id}-title"} class="text-lg font-semibold leading-8 text-gray-800">
2023-03-05 16:07:40 +01:00
<%= render_slot(@title) %>
</h1>
<p
:if={@subtitle != []}
id={"#{@id}-description"}
class="mt-2 text-sm leading-6 text-gray-600"
2023-03-05 16:07:40 +01:00
>
<%= render_slot(@subtitle) %>
</p>
</header>
<%= render_slot(@inner_block) %>
<div :if={@confirm != [] or @cancel != []} class="ml-6 mb-4 flex items-center gap-5">
<.button
:for={confirm <- @confirm}
id={"#{@id}-confirm"}
phx-click={@on_confirm}
phx-disable-with
class="py-2 px-3"
>
<%= render_slot(confirm) %>
</.button>
<.link
:for={cancel <- @cancel}
phx-click={hide_modal(@on_cancel, @id)}
class="text-sm font-semibold leading-6 text-gray-900 hover:text-gray-700"
2023-03-05 16:07:40 +01:00
>
<%= render_slot(cancel) %>
</.link>
</div>
</div>
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end
@doc """
Renders flash notices.
## Examples
<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
"""
attr :id, :string, default: "flash", 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 :autoshow, :boolean, default: true, doc: "whether to auto show the flash on mount"
attr :close, :boolean, default: true, doc: "whether the flash can be closed"
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
~H"""
<div
:if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
id={@id}
phx-mounted={@autoshow && show("##{@id}")}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class={[
"fixed hidden top-2 right-2 w-80 sm:w-96 z-50 rounded-lg p-3 shadow-md shadow-gray-900/5 ring-1",
2023-03-31 16:58:10 +02:00
@kind == :info &&
"bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900 dark:bg-emerald-900 dark:text-emerald-200 hover:opacity-40",
@kind == :error &&
"bg-rose-50 p-3 text-rose-900 shadow-md ring-rose-500 fill-rose-900 dark:bg-rose-900 dark:text-rose-200 hover:opacity-40"
2023-03-05 16:07:40 +01:00
]}
{@rest}
>
<p :if={@title} class="flex items-center gap-1.5 text-[0.8125rem] font-semibold leading-6">
<.icon :if={@kind == :info} name="hero-information-circle-mini" class="w-4 h-4" />
<.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="w-4 h-4" />
<%= @title %>
</p>
<p class="mt-2 text-[0.8125rem] leading-5"><%= msg %></p>
<button
:if={@close}
type="button"
class="group absolute top-2 right-1 p-2"
aria-label={gettext("close")}
>
<.icon name="hero-x-mark-solid" class="w-5 h-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"
def flash_group(assigns) do
~H"""
<.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} />
<.flash
id="disconnected"
kind={:error}
title="We can't find the internet"
close={false}
autoshow={false}
phx-disconnected={show("#disconnected")}
phx-connected={hide("#disconnected")}
>
Attempting to reconnect <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
</.flash>
"""
end
@doc """
Renders a simple form.
## Examples
<.simple_form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:email]} label="Email"/>
<.input field={@form[:username]} label="Username" />
<:actions>
<.button>Save</.button>
</:actions>
</.simple_form>
"""
attr :for, :any, required: true, doc: "the datastructure for the form"
attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
attr :class, :string, default: "shadow rounded mt-10 bg-white dark:bg-gray-900"
2023-03-05 16:07:40 +01:00
attr :rest, :global,
2023-03-06 23:15:23 +01:00
include: ~w(autocomplete name rel action enctype method novalidate target multipart),
2023-03-05 16:07:40 +01:00
doc: "the arbitrary HTML attributes to apply to the form tag"
slot :inner_block, required: true
slot :actions, doc: "the slot for form actions, such as a submit button"
def simple_form(assigns) do
~H"""
<.form :let={f} for={@for} as={@as} {@rest} class={@class}>
<div class="space-y-8">
2023-03-05 16:07:40 +01:00
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
<%= render_slot(action, f) %>
</div>
</div>
</.form>
"""
end
@doc """
Renders a button.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :rest, :global, include: ~w(disabled form name value)
slot :inner_block, required: true
def button(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-gray-900 hover:bg-gray-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80 dark:bg-gray-800 dark:hover:bg-gray-700",
2023-03-05 16:07:40 +01:00
@class
]}
{@rest}
>
<%= render_slot(@inner_block) %>
</button>
"""
end
@doc """
Renders an input with label and error messages.
A `%Phoenix.HTML.Form{}` and field name may be passed to the input
to build input names and error messages, or all the attributes and
errors may be passed explicitly.
## Examples
<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />
"""
attr :id, :any, default: nil
2023-03-30 22:55:24 +02:00
attr :class, :string, default: nil
2023-03-05 16:07:40 +01:00
attr :name, :any
attr :label, :string, default: nil
attr :value, :any
attr :type, :string,
default: "text",
values: ~w(checkbox color date datetime-local email file hidden month number password
range radio search select tel text textarea time url week)
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
attr :errors, :list, default: []
attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
2023-04-10 12:08:19 +02:00
attr :rest, :global,
include:
~w(autocomplete accept cols disabled form max maxlength min minlength pattern placeholder readonly required rows size step)
2023-04-10 12:08:19 +02:00
2023-03-05 16:07:40 +01:00
slot :inner_block
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
assigns
|> assign(field: nil, id: assigns.id || field.id)
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|> assign_new(:value, fn -> field.value end)
|> input()
end
def input(%{type: "checkbox", value: value} = assigns) do
assigns =
assign_new(assigns, :checked, fn -> Phoenix.HTML.Form.normalize_value("checkbox", value) end)
~H"""
<div phx-feedback-for={@name}>
<label class="flex items-center gap-4 text-sm leading-6 text-gray-600 dark:text-gray-300">
2023-03-05 16:07:40 +01:00
<input type="hidden" name={@name} value="false" />
<input
type="checkbox"
id={@id || @name}
name={@name}
value="true"
checked={@checked}
class="rounded border-gray-300 text-gray-900 focus:ring-gray-900 dark:text-gray-700 dark:focus:bg-gray-700 dark:border-gray-700 dark:bg-gray-700"
2023-03-05 16:07:40 +01:00
{@rest}
/>
<%= @label %>
</label>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "select"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<select
id={@id}
name={@name}
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-gray-500 focus:border-gray-500 sm:text-sm dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:focus:border-gray-400"
2023-03-05 16:07:40 +01:00
multiple={@multiple}
{@rest}
>
<option :if={@prompt} value=""><%= @prompt %></option>
<%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
</select>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<textarea
id={@id || @name}
name={@name}
class={[
"mt-2 block min-h-[6rem] w-full rounded-lg border-gray-300 py-[7px] px-[11px]",
"text-gray-900 focus:border-gray-400 focus:outline-none focus:ring-4 focus:ring-gray-800/5 sm:text-sm sm:leading-6",
"phx-no-feedback:border-gray-300 phx-no-feedback:focus:border-gray-400 phx-no-feedback:focus:ring-gray-800/5",
"border-gray-300 focus:border-gray-400 focus:ring-gray-800/5",
"dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:focus:border-gray-400",
2023-03-30 22:55:24 +02:00
@class,
2023-03-05 16:07:40 +01:00
@errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
]}
{@rest}
><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
def input(assigns) do
~H"""
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
name={@name}
id={@id || @name}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
class={[
"mt-2 block w-full rounded-lg border-gray-300 py-[7px] px-[11px]",
"text-gray-900 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
"phx-no-feedback:border-gray-300 phx-no-feedback:focus:border-gray-400 phx-no-feedback:focus:ring-gray-800/5",
"border-gray-300 focus:border-gray-400 focus:ring-gray-800/5",
"dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700 dark:focus:border-gray-400",
2023-03-30 22:55:24 +02:00
@class,
2023-03-05 16:07:40 +01:00
@errors != [] && "border-rose-400 focus:border-rose-400 focus:ring-rose-400/10"
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end
@doc """
Renders a label.
"""
attr :for, :string, default: nil
slot :inner_block, required: true
def label(assigns) do
~H"""
<label for={@for} class="block text-sm font-semibold leading-6 text-gray-800 dark:text-gray-300">
2023-03-05 16:07:40 +01:00
<%= render_slot(@inner_block) %>
</label>
"""
end
@doc """
Generates a generic error message.
"""
slot :inner_block, required: true
def error(assigns) do
~H"""
<p class="phx-no-feedback:hidden mt-3 flex gap-3 text-sm leading-6 text-rose-600">
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 w-5 h-5 flex-none" />
<%= render_slot(@inner_block) %>
</p>
"""
end
@doc """
Renders a header with title.
"""
attr :class, :string, default: nil
slot :inner_block, required: true
slot :subtitle
slot :actions
def header(assigns) do
~H"""
2023-06-03 11:47:55 +02:00
<header class={[@actions != [] && "flex justify-between gap-6 flex-col lg:flex-row", @class]}>
2023-03-05 16:07:40 +01:00
<div>
<h1 class="text-lg font-semibold leading-8 text-gray-800 dark:text-gray-200">
2023-03-05 16:07:40 +01:00
<%= render_slot(@inner_block) %>
</h1>
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">
2023-03-05 16:07:40 +01:00
<%= render_slot(@subtitle) %>
</p>
</div>
<div class="flex-none"><%= render_slot(@actions) %></div>
</header>
"""
end
@doc ~S"""
Renders a table with generic styling.
## Examples
<.table id="users" rows={@users}>
<:col :let={user} label="id"><%= user.id %></:col>
<:col :let={user} label="username"><%= user.username %></:col>
</.table>
"""
attr :id, :string, required: true
attr :rows, :list, required: true
attr :row_id, :any, default: nil, doc: "the function for generating the row id"
attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
attr :row_item, :any,
default: &Function.identity/1,
doc: "the function for mapping each row before calling the :col and :action slots"
slot :col, required: true do
attr :label, :string
end
slot :action, doc: "the slot for showing user actions in the last table column"
def table(assigns) do
assigns =
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
end
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible bg-white dark:bg-gray-950 pt-1 mt-10 rounded shadow">
<table class="mt-4 w-[40rem] sm:w-full">
<thead class="text-left text-[0.8125rem] leading-6 text-gray-500">
2023-03-05 16:07:40 +01:00
<tr>
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
<th class="relative p-0 pb-4"><span class="sr-only"><%= gettext("Actions") %></span></th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-gray-100 border-t border-gray-200 text-sm leading-6 text-gray-700 dark:text-gray-200 dark:border-gray-700 dark:divide-gray-800"
2023-03-05 16:07:40 +01:00
>
2023-03-31 16:58:10 +02:00
<tr
:for={row <- @rows}
id={@row_id && @row_id.(row)}
class="group hover:bg-gray-50 dark:hover:bg-gray-800"
>
2023-03-05 16:07:40 +01:00
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
>
<div class="block py-4 pr-6">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-gray-50 sm:rounded-l-xl dark:group-hover:bg-gray-800" />
<span class={["relative", i == 0 && "font-semibold text-gray-900 dark:text-gray-100"]}>
2023-03-05 16:07:40 +01:00
<%= render_slot(col, @row_item.(row)) %>
</span>
</div>
</td>
<td :if={@action != []} class="relative p-0 w-14">
<div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
<span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-gray-50 sm:rounded-r-xl dark:group-hover:bg-gray-800" />
2023-03-05 16:07:40 +01:00
<span
:for={action <- @action}
class="relative ml-4 font-semibold leading-6 text-gray-900 hover:text-gray-700 dark:text-gray-100 dark:hover:text-gray-300"
2023-03-05 16:07:40 +01:00
>
<%= render_slot(action, @row_item.(row)) %>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
"""
end
@doc """
Renders a data list.
## Examples
<.list>
<:item title="Title"><%= @post.title %></:item>
<:item title="Views"><%= @post.views %></:item>
</.list>
"""
slot :item, required: true do
attr :title, :string, required: true
end
def list(assigns) do
~H"""
<div class="mt-10 bg-white dark:bg-gray-950 rounded shadow px-4">
<dl class="-my-4 divide-y divide-gray-100 dark:divide-gray-800">
2023-03-05 16:07:40 +01:00
<div :for={item <- @item} class="flex gap-4 py-4 sm:gap-8">
2023-03-31 16:58:10 +02:00
<dt class="w-1/4 flex-none text-[0.8125rem] leading-6 text-gray-500 dark:text-gray-300">
<%= item.title %>
</dt>
<dd class="text-sm leading-6 text-gray-700 dark:text-gray-400"><%= render_slot(item) %></dd>
2023-03-05 16:07:40 +01:00
</div>
</dl>
</div>
"""
end
@doc """
Renders a back navigation link.
## Examples
<.back navigate={~p"/posts"}>Back to posts</.back>
"""
attr :navigate, :any, required: true
slot :inner_block, required: true
def back(assigns) do
~H"""
<div class="mt-16">
<.link
navigate={@navigate}
class="text-sm font-semibold leading-6 text-gray-900 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-400"
2023-03-05 16:07:40 +01:00
>
<.icon name="hero-arrow-left-solid" class="w-3 h-3" />
<%= render_slot(@inner_block) %>
</.link>
</div>
"""
end
@doc """
Renders a [Hero Icon](https://heroicons.com).
Hero icons come in three styles outline, solid, and mini.
By default, the outline style is used, but solid an 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 your `priv/hero_icons` directory and bundled
within your compiled app.css by the plugin in your `assets/tailwind.config.js`.
## Examples
<.icon name="hero-cake" />
<.icon name="hero-cake-solid" />
<.icon name="hero-cake-mini" />
<.icon name="hero-bolt" class="bg-blue-500 w-10 h-10" />
"""
attr :name, :string, required: true
attr :class, :string, default: nil
def icon(%{name: "hero-" <> _} = assigns) do
~H"""
<span class={[@name, @class]} />
"""
end
2023-06-19 21:49:01 +02:00
@doc """
Renders a horizontal line
"""
def line(assigns) do
~H"""
<hr class="my-6 dark:border-gray-700" />
"""
end
@doc """
Renders a UI for uploading files
"""
attr :upload, :map, required: true
attr :cancel_upload, :string, default: "cancel-upload"
def live_upload(assigns) do
~H"""
<div>
<.live_file_input upload={@upload} class="dark:text-gray-300" />
<section phx-drop-target={@upload.ref}>
<%= for entry <- @upload.entries do %>
<article class="upload-entry">
<figure>
<.live_img_preview entry={entry} />
<figcaption class="dark:text-gray-100"><%= entry.client_name %></figcaption>
</figure>
<div class="flex">
<%!-- entry.progress will update automatically for in-flight entries --%>
<progress value={entry.progress} max="100" class="w-full">
<%= entry.progress %>%
</progress>
<%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/3 --%>
<button
class="px-2 dark:text-white"
type="button"
phx-click="cancel-upload"
phx-value-ref={entry.ref}
aria-label="cancel"
>
&times;
</button>
</div>
<%!-- Phoenix.Component.upload_errors/2 returns a list of error atoms --%>
<%= for err <- upload_errors(@upload, entry) do %>
<p class="alert alert-danger"><%= upload_error_to_string(err) %></p>
<% end %>
</article>
<% end %>
</section>
</div>
"""
end
@doc """
Renders the admin menu bar
"""
attr :current_user, :map, required: true
attr :settings, :map, required: true
def admin_bar(assigns) do
~H"""
<ul class="sticky top-0 backdrop-blur-sm z-10 flex items-center gap-4 py-3 px-4 sm:px-6 lg:px-8 bg-white/30 dark:bg-black/30">
<li>
<.link
href={~p"/admin"}
2023-06-19 21:49:01 +02:00
class="flex gap-3 text-sm leading-6 font-semibold text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
Dashboard
</.link>
</li>
<li>
<.link
href={~p"/"}
class="text-sm leading-6 text-gray-900 dark:text-gray-100 dark:hover:text-gray-300 hover:text-gray-700"
>
<.icon name="hero-arrow-top-right-on-square" class="w-4 h-4 align-sub" /> Public Site
2023-06-19 21:49:01 +02:00
</.link>
</li>
<li class="flex-1"></li>
<li>
<.darkmode_toggle />
</li>
</ul>
"""
end
2023-03-05 16:07:40 +01:00
## JS Commands
def show(js \\ %JS{}, selector) do
JS.show(js,
to: selector,
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
def show_modal(js \\ %JS{}, id) when is_binary(id) do
js
|> JS.show(to: "##{id}")
|> JS.show(
to: "##{id}-bg",
transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
)
|> show("##{id}-container")
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: "##{id}-content")
end
def hide_modal(js \\ %JS{}, id) do
js
|> JS.hide(
to: "##{id}-bg",
transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
)
|> hide("##{id}-container")
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate "is invalid" in the "errors" domain
# dgettext("errors", "is invalid")
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# Because the error messages we show in our forms and APIs
# are defined inside Ecto, we need to translate them dynamically.
# This requires us to call the Gettext module passing our gettext
# backend as first argument.
#
# Note we use the "errors" domain, which means translations
# should be written to the errors.po file. The :count option is
# set by Ecto and indicates we should also apply plural rules.
if count = opts[:count] do
Gettext.dngettext(ChiyaWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(ChiyaWeb.Gettext, "errors", msg, opts)
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
2023-06-19 21:49:01 +02:00
defp upload_error_to_string(:too_large), do: "Too large"
defp upload_error_to_string(:too_many_files), do: "You have selected too many files"
defp upload_error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
2023-03-05 16:07:40 +01:00
end