defmodule Usher.Web.Live.InvitationsList do @moduledoc false use Usher.Web, :live_view alias Usher.Invitation alias Usher.Web.Components.InvitationFormComponent alias Usher.Web.Config import Usher.Web.Helpers.DateTimeHelpers @impl Phoenix.LiveView def render(assigns) do ~H"""

Invitations

View and manage invitations.

<.table id="invitations" rows={@streams.invitations}> <:col :let={{_id, invitation}} label="Name"> {invitation.name || "Unnamed"} <:col :let={{_id, invitation}} label="Created"> <:col :let={{_id, invitation}} label="Expires"> {expires_at(invitation)} <:col :let={{_id, invitation}} label="Usage">
{invitation.joined_count} {if invitation.joined_count == 1, do: "user", else: "users"}
<:col :let={{id, invitation}} label="Invitation Link">
<.link class="text-blue-600 hover:text-blue-800 text-sm font-mono truncate max-w-xs" href={token_link(invitation)} target="_blank" > {token_link(invitation) |> token_link_display_value()} <.button id={"copy-invitation-token-link-#{id}"} variant="light" class="text-zinc-700 bg-zinc-100 hover:bg-zinc-200 px-2 py-1" phx-hook="CopyToClipboard" data-clipboard-text={token_link(invitation)} title="Copy invitation link" > <.icon name="hero-clipboard" class="h-4 w-4" />
<:action :let={{_id, invitation}}>
<.link patch={usher_path([invitation.id, :edit])}> <.button variant="light" class="text-blue-700 bg-blue-100 hover:bg-blue-200 px-3 py-2" title="Edit invitation" > <.icon name="hero-pencil" class="h-4 w-4" /> <.button variant="light" class="text-red-700 bg-red-100 hover:bg-red-200 px-3 py-2" phx-click="delete_invitation" phx-value-id={invitation.id} title="Delete invitation" > <.icon name="hero-trash" class="h-4 w-4" />
<:empty_state>
<.icon name="hero-inbox" class="mx-auto h-12 w-12 text-zinc-400" />

No invitations yet

Get started by creating your first invitation.

<.link patch={usher_path([:new])}> <.button variant="primary"> New Invitation
<.modal :if={@live_action in [:new, :edit]} id="invitation-modal" show on_cancel={JS.patch(usher_path())} > <.live_component module={Usher.Web.Components.InvitationFormComponent} id={@invitation.id || :new} title={if @live_action == :new, do: "New Invitation", else: "Edit Invitation"} action={@live_action} invitation={@invitation} /> <.modal :if={@delete_confirmation} id="delete-confirmation-modal" show={true} on_cancel={JS.push("cancel_delete_invitation")} >

Confirm Deletion

Are you sure you want to delete this invitation? This action cannot be undone and will invalidate the invitation link.

<.button type="button" variant="light" phx-click="cancel_delete_invitation"> Cancel <.button type="button" variant="danger" phx-click="confirm_delete_invitation" phx-value-id={@delete_confirmation.id} > <.icon name="hero-trash" class="h-4 w-4 mr-1" /> Delete
""" end @impl Phoenix.LiveView def mount(_params, _session, socket) do invitations = Usher.list_invitations() |> Enum.map(&add_usage_count/1) socket = socket |> assign(:delete_confirmation, nil) |> stream(:invitations, invitations) {:ok, socket} end @impl Phoenix.LiveView def handle_params(params, _url, socket) do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end defp apply_action(socket, :index, _params) do socket |> assign(:invitation, nil) end defp apply_action(socket, :new, _params) do socket |> assign(:invitation, %Invitation{}) end defp apply_action(socket, :edit, %{"id" => id}) do invitation = Usher.get_invitation!(id) socket |> assign(:invitation, invitation) end @impl Phoenix.LiveView def handle_info({InvitationFormComponent, {:saved, %Invitation{} = new_invitation}}, socket) do new_invitation = add_usage_count(new_invitation) socket = socket |> stream_insert(:invitations, new_invitation, at: 0) {:noreply, socket} end @impl Phoenix.LiveView def handle_info( {InvitationFormComponent, {:updated, %Invitation{} = updated_invitation}}, socket ) do updated_invitation = add_usage_count(updated_invitation) socket = socket |> stream_insert(:invitations, updated_invitation, update_only: true) {:noreply, socket} end @impl Phoenix.LiveView def handle_event("delete_invitation", %{"id" => id}, socket) do invitation = Usher.get_invitation!(id) {:noreply, assign(socket, :delete_confirmation, invitation)} end @impl Phoenix.LiveView def handle_event("confirm_delete_invitation", %{"id" => id}, socket) do invitation = Usher.get_invitation!(id) {:ok, _} = Usher.delete_invitation(invitation) socket = socket |> stream_delete(:invitations, invitation) |> assign(:delete_confirmation, nil) |> put_flash(:info, "Invitation deleted successfully") {:noreply, socket} end @impl Phoenix.LiveView def handle_event("cancel_delete_invitation", _params, socket) do {:noreply, assign(socket, :delete_confirmation, nil)} end @impl Phoenix.LiveView def handle_event("copy-to-clipboard-success", _, socket) do socket = socket |> put_flash(:info, "Token link copied to clipboard") {:noreply, socket} end @impl Phoenix.LiveView def handle_event("copy-to-clipboard-error", %{"error" => error}, socket) do socket = socket |> put_flash(:error, "Failed to copy token link to clipboard: #{error}") {:noreply, socket} end defp token_link(invitation) do invitation_redirect_url = Config.invitation_redirect_url() Usher.invitation_url(invitation.token, invitation_redirect_url) end defp token_link_display_value(link) do String.slice(link, -1 * Usher.Config.token_length(), 20) end defp expires_at(%{expires_at: expires_at}) when is_struct(expires_at, DateTime) do time_left_until(expires_at) end defp expires_at(_), do: "Never" defp expires_at_class(invitation) do cond do is_nil(invitation.expires_at) -> "bg-green-100 text-green-800" DateTime.compare(invitation.expires_at, DateTime.utc_now()) == :lt -> "bg-red-100 text-red-800" DateTime.diff(invitation.expires_at, DateTime.utc_now(), :day) <= 7 -> "bg-yellow-100 text-yellow-800" true -> "bg-blue-100 text-blue-800" end end defp add_usage_count(invitation) do # Get unique users that registered using this invitation unique_registrations = Usher.list_invitation_usages_by_unique_entity( invitation, entity_type: :user, action: :registered ) Map.put(invitation, :joined_count, length(unique_registrations)) end end