forked from shibao/cannery
add invites route
This commit is contained in:
parent
70faed71d0
commit
c10cff63ea
@ -4,8 +4,8 @@ defmodule Lokal.Invites do
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias Lokal.{Accounts.User, Invites.Invite, Repo}
|
|
||||||
alias Ecto.Changeset
|
alias Ecto.Changeset
|
||||||
|
alias Lokal.{Accounts.User, Invites.Invite, Repo}
|
||||||
|
|
||||||
@invite_token_length 20
|
@invite_token_length 20
|
||||||
|
|
||||||
|
68
lib/lokal_web/live/invite_live/form_component.ex
Normal file
68
lib/lokal_web/live/invite_live/form_component.ex
Normal file
@ -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
|
33
lib/lokal_web/live/invite_live/form_component.html.heex
Normal file
33
lib/lokal_web/live/invite_live/form_component.html.heex
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<div>
|
||||||
|
<h2 class="mb-8 text-center title text-xl text-primary-600">
|
||||||
|
<%= @title %>
|
||||||
|
</h2>
|
||||||
|
<.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 %>
|
||||||
|
<div class="invalid-feedback col-span-3 text-center">
|
||||||
|
<%= changeset_errors(@changeset) %>
|
||||||
|
</div>
|
||||||
|
<% 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...")
|
||||||
|
) %>
|
||||||
|
</.form>
|
||||||
|
</div>
|
152
lib/lokal_web/live/invite_live/index.ex
Normal file
152
lib/lokal_web/live/invite_live/index.ex
Normal file
@ -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
|
156
lib/lokal_web/live/invite_live/index.html.heex
Normal file
156
lib/lokal_web/live/invite_live/index.html.heex
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
<div class="w-full flex flex-col space-y-8 justify-center items-center">
|
||||||
|
<h1 class="title text-2xl title-primary-500">
|
||||||
|
<%= gettext("Invites") %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<%= if @invites |> Enum.empty?() do %>
|
||||||
|
<h1 class="title text-xl text-primary-600">
|
||||||
|
<%= gettext("No invites 😔") %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<%= 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 %>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-row flex-wrap justify-center items-center">
|
||||||
|
<%= for invite <- @invites do %>
|
||||||
|
<.invite_card invite={invite}>
|
||||||
|
<:code_actions>
|
||||||
|
<form phx-submit="copy_to_clipboard">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="mx-2 my-1 btn btn-primary"
|
||||||
|
phx-click={JS.dispatch("lokal:clipcopy", to: "#code-#{invite.id}")}
|
||||||
|
>
|
||||||
|
<%= dgettext("actions", "Copy to clipboard") %>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</:code_actions>
|
||||||
|
<%= live_patch to: Routes.invite_index_path(Endpoint, :edit, invite),
|
||||||
|
class: "text-primary-600 link",
|
||||||
|
data: [qa: "edit-#{invite.id}"] do %>
|
||||||
|
<i class="fa-fw fa-lg fas fa-edit"></i>
|
||||||
|
<% 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 %>
|
||||||
|
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if invite.disabled_at |> is_nil() do %>
|
||||||
|
<a href="#" class="btn btn-primary" phx-click="disable_invite" phx-value-id={invite.id}>
|
||||||
|
<%= gettext("Disable") %>
|
||||||
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<a href="#" class="btn btn-primary" phx-click="enable_invite" phx-value-id={invite.id}>
|
||||||
|
<%= gettext("Enable") %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= if invite.disabled_at |> is_nil() and not (invite.uses_left |> is_nil()) do %>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="btn btn-primary"
|
||||||
|
phx-click="set_unlimited"
|
||||||
|
phx-value-id={invite.id}
|
||||||
|
data-confirm={
|
||||||
|
dgettext("prompts", "Are you sure you want to make %{name} unlimited?",
|
||||||
|
name: invite.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<%= gettext("Set Unlimited") %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</.invite_card>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= unless @admins |> Enum.empty?() do %>
|
||||||
|
<hr class="hr" />
|
||||||
|
|
||||||
|
<h1 class="title text-2xl text-primary-600">
|
||||||
|
<%= gettext("Admins") %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-row flex-wrap justify-center items-center">
|
||||||
|
<%= 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 %>
|
||||||
|
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||||
|
<% end %>
|
||||||
|
</.user_card>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= unless @users |> Enum.empty?() do %>
|
||||||
|
<hr class="hr" />
|
||||||
|
|
||||||
|
<h1 class="title text-2xl text-primary-600">
|
||||||
|
<%= gettext("Users") %>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="w-full flex flex-row flex-wrap justify-center items-center">
|
||||||
|
<%= 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 %>
|
||||||
|
<i class="fa-fw fa-lg fas fa-trash"></i>
|
||||||
|
<% end %>
|
||||||
|
</.user_card>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= 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}
|
||||||
|
/>
|
||||||
|
</.modal>
|
||||||
|
<% end %>
|
Loading…
Reference in New Issue
Block a user