diff --git a/lib/lokal/invites.ex b/lib/lokal/invites.ex
index 1ae9688c..4e03b375 100644
--- a/lib/lokal/invites.ex
+++ b/lib/lokal/invites.ex
@@ -4,8 +4,8 @@ defmodule Lokal.Invites do
"""
import Ecto.Query, warn: false
- alias Lokal.{Accounts.User, Invites.Invite, Repo}
alias Ecto.Changeset
+ alias Lokal.{Accounts.User, Invites.Invite, Repo}
@invite_token_length 20
diff --git a/lib/lokal_web/live/invite_live/form_component.ex b/lib/lokal_web/live/invite_live/form_component.ex
new file mode 100644
index 00000000..36b670ce
--- /dev/null
+++ b/lib/lokal_web/live/invite_live/form_component.ex
@@ -0,0 +1,68 @@
+defmodule LokalWeb.InviteLive.FormComponent do
+ @moduledoc """
+ Livecomponent that can update or create an Lokal.Invites.Invite
+ """
+
+ use LokalWeb, :live_component
+ alias Lokal.{Accounts.User, Invites, Invites.Invite}
+ alias Ecto.Changeset
+ alias Phoenix.LiveView.Socket
+
+ @impl true
+ @spec update(
+ %{:invite => Invite.t(), :current_user => User.t(), optional(any) => any},
+ Socket.t()
+ ) :: {:ok, Socket.t()}
+ def update(%{invite: invite} = assigns, socket) do
+ {:ok, socket |> assign(assigns) |> assign(:changeset, Invites.change_invite(invite))}
+ end
+
+ @impl true
+ def handle_event(
+ "validate",
+ %{"invite" => invite_params},
+ %{assigns: %{invite: invite}} = socket
+ ) do
+ {:noreply, socket |> assign(:changeset, invite |> Invites.change_invite(invite_params))}
+ end
+
+ def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do
+ save_invite(socket, action, invite_params)
+ end
+
+ defp save_invite(
+ %{assigns: %{current_user: current_user, invite: invite, return_to: return_to}} = socket,
+ :edit,
+ invite_params
+ ) do
+ socket =
+ case invite |> Invites.update_invite(invite_params, current_user) do
+ {:ok, %{name: invite_name}} ->
+ prompt = dgettext("prompts", "%{name} updated successfully", name: invite_name)
+ socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
+
+ {:error, %Changeset{} = changeset} ->
+ socket |> assign(:changeset, changeset)
+ end
+
+ {:noreply, socket}
+ end
+
+ defp save_invite(
+ %{assigns: %{current_user: current_user, return_to: return_to}} = socket,
+ :new,
+ invite_params
+ ) do
+ socket =
+ case current_user |> Invites.create_invite(invite_params) do
+ {:ok, %{name: invite_name}} ->
+ prompt = dgettext("prompts", "%{name} created successfully", name: invite_name)
+ socket |> put_flash(:info, prompt) |> push_redirect(to: return_to)
+
+ {:error, %Changeset{} = changeset} ->
+ socket |> assign(changeset: changeset)
+ end
+
+ {:noreply, socket}
+ end
+end
diff --git a/lib/lokal_web/live/invite_live/form_component.html.heex b/lib/lokal_web/live/invite_live/form_component.html.heex
new file mode 100644
index 00000000..27c2fa24
--- /dev/null
+++ b/lib/lokal_web/live/invite_live/form_component.html.heex
@@ -0,0 +1,33 @@
+
+
+ <%= @title %>
+
+ <.form
+ let={f}
+ for={@changeset}
+ id="invite-form"
+ class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <%= if @changeset.action && not @changeset.valid? do %>
+
+ <%= changeset_errors(@changeset) %>
+
+ <% end %>
+
+ <%= label(f, :name, gettext("Name"), class: "title text-lg text-primary-600") %>
+ <%= text_input(f, :name, class: "input input-primary col-span-2") %>
+ <%= error_tag(f, :name, "col-span-3") %>
+
+ <%= label(f, :uses_left, gettext("Uses left"), class: "title text-lg text-primary-600") %>
+ <%= number_input(f, :uses_left, min: 0, class: "input input-primary col-span-2") %>
+ <%= error_tag(f, :uses_left, "col-span-3") %>
+
+ <%= submit(dgettext("actions", "Save"),
+ class: "mx-auto btn btn-primary col-span-3",
+ phx_disable_with: dgettext("prompts", "Saving...")
+ ) %>
+
+
diff --git a/lib/lokal_web/live/invite_live/index.ex b/lib/lokal_web/live/invite_live/index.ex
new file mode 100644
index 00000000..80aeb9ad
--- /dev/null
+++ b/lib/lokal_web/live/invite_live/index.ex
@@ -0,0 +1,152 @@
+defmodule LokalWeb.InviteLive.Index do
+ @moduledoc """
+ Liveview to show a Lokal.Invites.Invite index
+ """
+
+ use LokalWeb, :live_view
+ import LokalWeb.Components.{InviteCard, UserCard}
+ alias Lokal.{Accounts, Invites, Invites.Invite}
+ alias LokalWeb.{Endpoint, PageLive}
+ alias Phoenix.LiveView.JS
+
+ @impl true
+ def mount(_params, session, socket) do
+ %{assigns: %{current_user: current_user}} = socket = socket |> assign_defaults(session)
+
+ socket =
+ if current_user |> Map.get(:role) == :admin do
+ socket |> display_invites()
+ else
+ prompt = dgettext("errors", "You are not authorized to view this page")
+ return_to = Routes.live_path(Endpoint, PageLive)
+ socket |> put_flash(:error, prompt) |> push_redirect(to: return_to)
+ end
+
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_params(params, _url, %{assigns: %{live_action: live_action}} = socket) do
+ {:noreply, socket |> apply_action(live_action, params)}
+ end
+
+ defp apply_action(%{assigns: %{current_user: current_user}} = socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(page_title: gettext("Edit Invite"), invite: Invites.get_invite!(id, current_user))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket |> assign(page_title: gettext("New Invite"), invite: %Invite{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket |> assign(page_title: gettext("Invites"), invite: nil)
+ end
+
+ @impl true
+ def handle_event(
+ "delete_invite",
+ %{"id" => id},
+ %{assigns: %{current_user: current_user}} = socket
+ ) do
+ %{name: invite_name} =
+ id |> Invites.get_invite!(current_user) |> Invites.delete_invite!(current_user)
+
+ prompt = dgettext("prompts", "%{name} deleted succesfully", name: invite_name)
+ {:noreply, socket |> put_flash(:info, prompt) |> display_invites()}
+ end
+
+ def handle_event(
+ "set_unlimited",
+ %{"id" => id},
+ %{assigns: %{current_user: current_user}} = socket
+ ) do
+ socket =
+ Invites.get_invite!(id, current_user)
+ |> Invites.update_invite(%{"uses_left" => nil}, current_user)
+ |> case do
+ {:ok, %{name: invite_name}} ->
+ prompt = dgettext("prompts", "%{name} updated succesfully", name: invite_name)
+ socket |> put_flash(:info, prompt) |> display_invites()
+
+ {:error, changeset} ->
+ socket |> put_flash(:error, changeset |> changeset_errors())
+ end
+
+ {:noreply, socket}
+ end
+
+ def handle_event(
+ "enable_invite",
+ %{"id" => id},
+ %{assigns: %{current_user: current_user}} = socket
+ ) do
+ socket =
+ Invites.get_invite!(id, current_user)
+ |> Invites.update_invite(%{"uses_left" => nil, "disabled_at" => nil}, current_user)
+ |> case do
+ {:ok, %{name: invite_name}} ->
+ prompt = dgettext("prompts", "%{name} enabled succesfully", name: invite_name)
+ socket |> put_flash(:info, prompt) |> display_invites()
+
+ {:error, changeset} ->
+ socket |> put_flash(:error, changeset |> changeset_errors())
+ end
+
+ {:noreply, socket}
+ end
+
+ def handle_event(
+ "disable_invite",
+ %{"id" => id},
+ %{assigns: %{current_user: current_user}} = socket
+ ) do
+ now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second)
+
+ socket =
+ Invites.get_invite!(id, current_user)
+ |> Invites.update_invite(%{"uses_left" => 0, "disabled_at" => now}, current_user)
+ |> case do
+ {:ok, %{name: invite_name}} ->
+ prompt = dgettext("prompts", "%{name} disabled succesfully", name: invite_name)
+ socket |> put_flash(:info, prompt) |> display_invites()
+
+ {:error, changeset} ->
+ socket |> put_flash(:error, changeset |> changeset_errors())
+ end
+
+ {:noreply, socket}
+ end
+
+ @impl true
+ def handle_event("copy_to_clipboard", _, socket) do
+ prompt = dgettext("prompts", "Copied to clipboard")
+ {:noreply, socket |> put_flash(:info, prompt)}
+ end
+
+ @impl true
+ def handle_event(
+ "delete_user",
+ %{"id" => id},
+ %{assigns: %{current_user: current_user}} = socket
+ ) do
+ %{email: user_email} = Accounts.get_user!(id) |> Accounts.delete_user!(current_user)
+
+ prompt = dgettext("prompts", "%{name} deleted succesfully", name: user_email)
+
+ {:noreply, socket |> put_flash(:info, prompt) |> display_invites()}
+ end
+
+ defp display_invites(%{assigns: %{current_user: current_user}} = socket) do
+ invites = Invites.list_invites(current_user)
+ all_users = Accounts.list_all_users_by_role(current_user)
+
+ admins =
+ all_users
+ |> Map.get(:admin, [])
+ |> Enum.reject(fn %{id: user_id} -> user_id == current_user.id end)
+
+ users = all_users |> Map.get(:user, [])
+ socket |> assign(invites: invites, admins: admins, users: users)
+ end
+end
diff --git a/lib/lokal_web/live/invite_live/index.html.heex b/lib/lokal_web/live/invite_live/index.html.heex
new file mode 100644
index 00000000..1b8c8644
--- /dev/null
+++ b/lib/lokal_web/live/invite_live/index.html.heex
@@ -0,0 +1,156 @@
+
+
+ <%= gettext("Invites") %>
+
+
+ <%= if @invites |> Enum.empty?() do %>
+
+ <%= gettext("No invites 😔") %>
+
+
+ <%= live_patch(dgettext("actions", "Invite someone new!"),
+ to: Routes.invite_index_path(Endpoint, :new),
+ class: "btn btn-primary"
+ ) %>
+ <% else %>
+ <%= live_patch(dgettext("actions", "Create Invite"),
+ to: Routes.invite_index_path(Endpoint, :new),
+ class: "btn btn-primary"
+ ) %>
+ <% end %>
+
+
+ <%= for invite <- @invites do %>
+ <.invite_card invite={invite}>
+ <:code_actions>
+
+
+ <%= live_patch to: Routes.invite_index_path(Endpoint, :edit, invite),
+ class: "text-primary-600 link",
+ data: [qa: "edit-#{invite.id}"] do %>
+
+ <% end %>
+
+ <%= link to: "#",
+ class: "text-primary-600 link",
+ phx_click: "delete_invite",
+ phx_value_id: invite.id,
+ data: [
+ confirm:
+ dgettext("prompts", "Are you sure you want to delete the invite for %{name}?",
+ name: invite.name
+ ),
+ qa: "delete-#{invite.id}"
+ ] do %>
+
+ <% end %>
+
+ <%= if invite.disabled_at |> is_nil() do %>
+
+ <%= gettext("Disable") %>
+
+ <% else %>
+
+ <%= gettext("Enable") %>
+
+ <% end %>
+
+ <%= if invite.disabled_at |> is_nil() and not (invite.uses_left |> is_nil()) do %>
+
+ <%= gettext("Set Unlimited") %>
+
+ <% end %>
+
+ <% end %>
+
+
+ <%= unless @admins |> Enum.empty?() do %>
+
+
+
+ <%= gettext("Admins") %>
+
+
+
+ <%= for admin <- @admins do %>
+ <.user_card user={admin}>
+ <%= link to: "#",
+ class: "text-primary-600 link",
+ phx_click: "delete_user",
+ phx_value_id: admin.id,
+ data: [
+ confirm:
+ dgettext(
+ "prompts",
+ "Are you sure you want to delete %{email}? This action is permanent!",
+ email: admin.email
+ )
+ ] do %>
+
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+
+ <%= unless @users |> Enum.empty?() do %>
+
+
+
+ <%= gettext("Users") %>
+
+
+
+ <%= for user <- @users do %>
+ <.user_card user={user}>
+ <%= link to: "#",
+ class: "text-primary-600 link",
+ phx_click: "delete_user",
+ phx_value_id: user.id,
+ data: [
+ confirm:
+ dgettext(
+ "prompts",
+ "Are you sure you want to delete %{email}? This action is permanent!",
+ email: user.email
+ )
+ ] do %>
+
+ <% end %>
+
+ <% end %>
+
+ <% end %>
+
+
+<%= if @live_action in [:new, :edit] do %>
+ <.modal return_to={Routes.invite_index_path(Endpoint, :index)}>
+ <.live_component
+ module={LokalWeb.InviteLive.FormComponent}
+ id={@invite.id || :new}
+ title={@page_title}
+ action={@live_action}
+ invite={@invite}
+ return_to={Routes.invite_index_path(Endpoint, :index)}
+ current_user={@current_user}
+ />
+
+<% end %>