diff --git a/lib/lokal/invites.ex b/lib/lokal/invites.ex index 1ae9688..4e03b37 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 0000000..36b670c --- /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 0000000..27c2fa2 --- /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 0000000..80aeb9a --- /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 0000000..1b8c864 --- /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 %>