feat: improve email copy UX with colored alerts and mailto button
All checks were successful
continuous-integration/drone/push Build is passing

- Green success alert for copied confirmation
- Blue info alert with BCC privacy tip
- Mailto button opens email program with BCC recipients
- Alerts stack vertically instead of overlapping
This commit is contained in:
Moritz 2025-12-02 11:42:11 +01:00
parent e2ace3d2a8
commit ba78a6ac7a
7 changed files with 159 additions and 94 deletions

View file

@ -42,7 +42,11 @@ defmodule MvWeb.CoreComponents do
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 :kind, :atom,
values: [:info, :error, :success, :warning],
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"
@ -56,25 +60,27 @@ defmodule MvWeb.CoreComponents do
id={@id}
phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
role="alert"
class="toast toast-top toast-end z-50"
{@rest}
>
<div class={[
class={[
"alert w-80 sm:w-96 max-w-80 sm:max-w-96 text-wrap",
@kind == :info && "alert-info",
@kind == :error && "alert-error"
]}>
<.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
<.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
<div>
<p :if={@title} class="font-semibold">{@title}</p>
<p>{msg}</p>
</div>
<div class="flex-1" />
<button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
<.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
</button>
@kind == :error && "alert-error",
@kind == :success && "bg-green-500 text-white",
@kind == :warning && "bg-blue-100 text-blue-800 border border-blue-300"
]}
{@rest}
>
<.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
<.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
<.icon :if={@kind == :success} name="hero-check-circle" class="size-5 shrink-0" />
<.icon :if={@kind == :warning} name="hero-information-circle" class="size-5 shrink-0" />
<div>
<p :if={@title} class="font-semibold">{@title}</p>
<p>{msg}</p>
</div>
<div class="flex-1" />
<button type="button" class="group self-start cursor-pointer" aria-label={gettext("close")}>
<.icon name="hero-x-mark" class="size-5 opacity-40 group-hover:opacity-70" />
</button>
</div>
"""
end

View file

@ -65,7 +65,9 @@ defmodule MvWeb.Layouts do
def flash_group(assigns) do
~H"""
<div id={@id} aria-live="polite">
<div id={@id} aria-live="polite" class="toast toast-top toast-end z-50 flex flex-col gap-2">
<.flash kind={:success} flash={@flash} />
<.flash kind={:warning} flash={@flash} />
<.flash kind={:info} flash={@flash} />
<.flash kind={:error} flash={@flash} />

View file

@ -146,7 +146,7 @@ defmodule MvWeb.MemberLive.Index do
socket
|> push_event("copy_to_clipboard", %{text: email_string})
|> put_flash(
:info,
:success,
ngettext(
"Copied %{count} email address to clipboard",
"Copied %{count} email addresses to clipboard",
@ -154,6 +154,10 @@ defmodule MvWeb.MemberLive.Index do
count: email_count
)
)
|> put_flash(
:warning,
gettext("Tip: Paste email addresses into the BCC field for privacy compliance")
)
{:noreply, socket}
end

View file

@ -12,6 +12,14 @@
<.icon name="hero-clipboard-document" />
{gettext("Copy emails")} ({Enum.count(@members, &(&1.id in @selected_members))})
</.button>
<.button
:if={Enum.any?(@members, &(&1.id in @selected_members))}
href={"mailto:?bcc=#{@members |> Enum.filter(&(&1.id in @selected_members and &1.email)) |> Enum.map(& &1.email) |> Enum.join(",")}"}
aria-label={gettext("Open email program with BCC recipients")}
>
<.icon name="hero-envelope" />
{gettext("Open in email program")}
</.button>
<.button variant="primary" navigate={~p"/members/new"}>
<.icon name="hero-plus" /> {gettext("New Member")}
</.button>