improve invites, record usage

This commit is contained in:
2023-02-04 17:22:06 -05:00
parent eb75937587
commit cd7220cea3
37 changed files with 902 additions and 614 deletions

View File

@ -4,9 +4,19 @@ defmodule MemexWeb.Components.InviteCard do
"""
use MemexWeb, :component
alias Memex.Accounts.{Invite, Invites, User}
alias MemexWeb.Endpoint
def invite_card(assigns) do
assigns = assigns |> assign_new(:code_actions, fn -> [] end)
attr :invite, Invite, required: true
attr :current_user, User, required: true
slot(:inner_block)
slot(:code_actions)
def invite_card(%{invite: invite, current_user: current_user} = assigns) do
assigns =
assigns
|> assign(:use_count, Invites.get_use_count(invite, current_user))
|> assign_new(:code_actions, fn -> [] end)
~H"""
<div class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center space-y-4
@ -20,8 +30,8 @@ defmodule MemexWeb.Components.InviteCard do
<h2 class="title text-md">
<%= if @invite.uses_left do %>
<%= gettext(
"uses left: %{uses_left}",
uses_left: @invite.uses_left
"uses left: %{uses_left_count}",
uses_left_count: @invite.uses_left
) %>
<% else %>
<%= gettext("uses left: unlimited") %>
@ -38,6 +48,10 @@ defmodule MemexWeb.Components.InviteCard do
filename={@invite.name}
/>
<h2 :if={@use_count != 0} class="title text-md">
<%= gettext("uses: %{uses_count}", uses_count: @use_count) %>
</h2>
<div class="flex flex-row flex-wrap justify-center items-center">
<code
id={"code-#{@invite.id}"}

View File

@ -1,14 +1,12 @@
defmodule MemexWeb.UserRegistrationController do
use MemexWeb, :controller
import MemexWeb.Gettext
alias Memex.{Accounts, Invites}
alias MemexWeb.HomeLive
alias Memex.{Accounts, Accounts.Invites}
alias MemexWeb.{Endpoint, HomeLive}
def new(conn, %{"invite" => invite_token}) do
invite = Invites.get_invite_by_token(invite_token)
if invite do
conn |> render_new(invite)
if Invites.valid_invite_token?(invite_token) do
conn |> render_new(invite_token)
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
@ -27,19 +25,17 @@ defmodule MemexWeb.UserRegistrationController do
end
# renders new user registration page
defp render_new(conn, invite \\ nil) do
defp render_new(conn, invite_token \\ nil) do
render(conn, "new.html",
changeset: Accounts.change_user_registration(),
invite: invite,
invite_token: invite_token,
page_title: gettext("register")
)
end
def create(conn, %{"user" => %{"invite_token" => invite_token}} = attrs) do
invite = Invites.get_invite_by_token(invite_token)
if invite do
conn |> create_user(attrs, invite)
if Invites.valid_invite_token?(invite_token) do
conn |> create_user(attrs, invite_token)
else
conn
|> put_flash(:error, dgettext("errors", "Sorry, this invite was not found or expired"))
@ -57,24 +53,25 @@ defmodule MemexWeb.UserRegistrationController do
end
end
defp create_user(conn, %{"user" => user_params}, invite \\ nil) do
case Accounts.register_user(user_params) do
defp create_user(conn, %{"user" => user_params}, invite_token \\ nil) do
case Accounts.register_user(user_params, invite_token) do
{:ok, user} ->
unless invite |> is_nil() do
invite |> Invites.use_invite!()
end
Accounts.deliver_user_confirmation_instructions(
user,
&Routes.user_confirmation_url(conn, :confirm, &1)
)
conn
|> put_flash(:info, dgettext("prompts", "Please check your email to verify your account"))
|> put_flash(:info, dgettext("prompts", "please check your email to verify your account"))
|> redirect(to: Routes.user_session_path(Endpoint, :new))
{:error, :invalid_token} ->
conn
|> put_flash(:error, dgettext("errors", "sorry, this invite was not found or expired"))
|> redirect(to: Routes.live_path(Endpoint, HomeLive))
{:error, %Ecto.Changeset{} = changeset} ->
conn |> render("new.html", changeset: changeset, invite: invite)
conn |> render("new.html", changeset: changeset, invite_token: invite_token)
end
end
end

View File

@ -7,7 +7,7 @@ defmodule MemexWeb.UserSettingsController do
plug :assign_email_and_password_changesets
def edit(conn, _params) do
render(conn, "edit.html", page_title: gettext("Settings"))
render(conn, "edit.html", page_title: gettext("settings"))
end
def update(%{assigns: %{current_user: user}} = conn, %{
@ -28,7 +28,7 @@ defmodule MemexWeb.UserSettingsController do
:info,
dgettext(
"prompts",
"A link to confirm your email change has been sent to the new address."
"a link to confirm your email change has been sent to the new address."
)
)
|> redirect(to: Routes.user_settings_path(conn, :edit))
@ -46,7 +46,7 @@ defmodule MemexWeb.UserSettingsController do
case Accounts.update_user_password(user, password, user_params) do
{:ok, user} ->
conn
|> put_flash(:info, dgettext("prompts", "Password updated successfully."))
|> put_flash(:info, dgettext("prompts", "password updated successfully."))
|> put_session(:user_return_to, Routes.user_settings_path(conn, :edit))
|> UserAuth.log_in_user(user)
@ -74,14 +74,14 @@ defmodule MemexWeb.UserSettingsController do
case Accounts.update_user_email(user, token) do
:ok ->
conn
|> put_flash(:info, dgettext("prompts", "Email changed successfully."))
|> put_flash(:info, dgettext("prompts", "email changed successfully."))
|> redirect(to: Routes.user_settings_path(conn, :edit))
:error ->
conn
|> put_flash(
:error,
dgettext("errors", "Email change link is invalid or it has expired.")
dgettext("errors", "email change link is invalid or it has expired.")
)
|> redirect(to: Routes.user_settings_path(conn, :edit))
end
@ -92,11 +92,11 @@ defmodule MemexWeb.UserSettingsController do
current_user |> Accounts.delete_user!(current_user)
conn
|> put_flash(:error, dgettext("prompts", "Your account has been deleted"))
|> put_flash(:error, dgettext("prompts", "your account has been deleted"))
|> redirect(to: Routes.live_path(conn, HomeLive))
else
conn
|> put_flash(:error, dgettext("errors", "Unable to delete user"))
|> put_flash(:error, dgettext("errors", "unable to delete user"))
|> redirect(to: Routes.user_settings_path(conn, :edit))
end
end

View File

@ -44,7 +44,7 @@
</div>
<.modal
if={@live_action == :edit}
:if={@live_action == :edit}
return_to={Routes.context_show_path(@socket, :show, @context.slug)}
>
<.live_component

View File

@ -1,11 +1,11 @@
defmodule MemexWeb.InviteLive.FormComponent do
@moduledoc """
Livecomponent that can update or create an Memex.Invites.Invite
Livecomponent that can update or create an Memex.Accounts.Invite
"""
use MemexWeb, :live_component
alias Ecto.Changeset
alias Memex.{Accounts.User, Invites, Invites.Invite}
alias Memex.Accounts.{Invite, Invites, User}
alias Phoenix.LiveView.Socket
@impl true
@ -13,23 +13,44 @@ defmodule MemexWeb.InviteLive.FormComponent do
%{: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))}
def update(%{invite: _invite} = assigns, socket) do
{:ok, socket |> assign(assigns) |> assign_changeset(%{})}
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))}
def handle_event("validate", %{"invite" => invite_params}, socket) do
{:noreply, socket |> assign_changeset(invite_params)}
end
def handle_event("save", %{"invite" => invite_params}, %{assigns: %{action: action}} = socket) do
save_invite(socket, action, invite_params)
end
defp assign_changeset(
%{assigns: %{action: action, current_user: user, invite: invite}} = socket,
invite_params
) do
changeset_action =
case action do
:new -> :insert
:edit -> :update
end
changeset =
case action do
:new -> Invite.create_changeset(user, "example_token", invite_params)
:edit -> invite |> Invite.update_changeset(invite_params)
end
changeset =
case changeset |> Changeset.apply_action(changeset_action) do
{:ok, _data} -> changeset
{:error, changeset} -> changeset
end
socket |> assign(:changeset, changeset)
end
defp save_invite(
%{assigns: %{current_user: current_user, invite: invite, return_to: return_to}} = socket,
:edit,
@ -38,9 +59,7 @@ defmodule MemexWeb.InviteLive.FormComponent do
socket =
case invite |> Invites.update_invite(invite_params, current_user) do
{:ok, %{name: invite_name}} ->
prompt =
dgettext("prompts", "%{invite_name} updated successfully", invite_name: invite_name)
prompt = dgettext("prompts", "%{name} updated successfully", name: invite_name)
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
{:error, %Changeset{} = changeset} ->
@ -58,9 +77,7 @@ defmodule MemexWeb.InviteLive.FormComponent do
socket =
case current_user |> Invites.create_invite(invite_params) do
{:ok, %{name: invite_name}} ->
prompt =
dgettext("prompts", "%{invite_name} created successfully", invite_name: invite_name)
prompt = dgettext("prompts", "%{name} created successfully", name: invite_name)
socket |> put_flash(:info, prompt) |> push_navigate(to: return_to)
{:error, %Changeset{} = changeset} ->

View File

@ -1,12 +1,13 @@
defmodule MemexWeb.InviteLive.Index do
@moduledoc """
Liveview to show a Memex.Invites.Invite index
Liveview to show a Memex.Accounts.Invite index
"""
use MemexWeb, :live_view
import MemexWeb.Components.{InviteCard, UserCard}
alias Memex.{Accounts, Invites, Invites.Invite}
alias MemexWeb.HomeLive
alias Memex.Accounts
alias Memex.Accounts.{Invite, Invites}
alias MemexWeb.{Endpoint, HomeLive}
alias Phoenix.LiveView.JS
@impl true
@ -15,9 +16,9 @@ defmodule MemexWeb.InviteLive.Index do
if current_user |> Map.get(:role) == :admin do
socket |> display_invites()
else
prompt = dgettext("errors", "You are not authorized to view this page")
prompt = dgettext("errors", "you are not authorized to view this page")
return_to = Routes.live_path(Endpoint, HomeLive)
socket |> put_flash(:error, prompt) |> push_navigate(to: return_to)
socket |> put_flash(:error, prompt) |> push_redirect(to: return_to)
end
{:ok, socket}
@ -61,7 +62,7 @@ defmodule MemexWeb.InviteLive.Index do
) do
socket =
Invites.get_invite!(id, current_user)
|> Invites.update_invite(%{"uses_left" => nil}, current_user)
|> Invites.update_invite(%{uses_left: nil}, current_user)
|> case do
{:ok, %{name: invite_name}} ->
prompt =
@ -83,7 +84,7 @@ defmodule MemexWeb.InviteLive.Index do
) do
socket =
Invites.get_invite!(id, current_user)
|> Invites.update_invite(%{"uses_left" => nil, "disabled_at" => nil}, current_user)
|> Invites.update_invite(%{uses_left: nil, disabled_at: nil}, current_user)
|> case do
{:ok, %{name: invite_name}} ->
prompt =
@ -107,7 +108,7 @@ defmodule MemexWeb.InviteLive.Index do
socket =
Invites.get_invite!(id, current_user)
|> Invites.update_invite(%{"uses_left" => 0, "disabled_at" => now}, current_user)
|> Invites.update_invite(%{uses_left: 0, disabled_at: now}, current_user)
|> case do
{:ok, %{name: invite_name}} ->
prompt =
@ -124,7 +125,7 @@ defmodule MemexWeb.InviteLive.Index do
@impl true
def handle_event("copy_to_clipboard", _, socket) do
prompt = dgettext("prompts", "Copied to clipboard")
prompt = dgettext("prompts", "copied to clipboard")
{:noreply, socket |> put_flash(:info, prompt)}
end

View File

@ -18,7 +18,7 @@
<% end %>
<div class="w-full flex flex-row flex-wrap justify-center items-center">
<.invite_card :for={invite <- @invites} invite={invite}>
<.invite_card :for={invite <- @invites} invite={invite} current_user={@current_user}>
<:code_actions>
<form phx-submit="copy_to_clipboard">
<button

View File

@ -1,6 +1,6 @@
defmodule MemexWeb.LiveHelpers do
@moduledoc """
Contains resuable methods for all liveviews
Contains common helper functions for liveviews
"""
import Phoenix.Component
@ -31,7 +31,7 @@ defmodule MemexWeb.LiveHelpers do
patch={@return_to}
id="modal-bg"
class="fade-in fixed z-10 left-0 top-0
w-screen h-screen overflow-hidden
w-full h-full overflow-hidden
p-8 flex flex-col justify-center items-center cursor-auto"
style="background-color: rgba(0,0,0,0.4);"
phx-remove={hide_modal()}
@ -63,7 +63,7 @@ defmodule MemexWeb.LiveHelpers do
<i class="fa-fw fa-lg fas fa-times"></i>
</.link>
<div class="overflow-x-hidden overflow-y-visible p-8 flex flex-col space-y-4 justify-start items-stretch">
<div class="overflow-x-hidden overflow-y-auto w-full p-8 flex flex-col space-y-4 justify-start items-stretch">
<%= render_slot(@inner_block) %>
</div>
</div>
@ -71,10 +71,57 @@ defmodule MemexWeb.LiveHelpers do
"""
end
def hide_modal(js \\ %JS{}) do
defp hide_modal(js \\ %JS{}) do
js
|> JS.hide(to: "#modal", transition: "fade-out")
|> JS.hide(to: "#modal-bg", transition: "fade-out")
|> JS.hide(to: "#modal-content", transition: "fade-out-scale")
end
@doc """
A toggle button element that can be directed to a liveview or a
live_component's `handle_event/3`.
## Examples
<.toggle_button action="my_liveview_action" value={@some_value}>
<span>Toggle me!</span>
</.toggle_button>
<.toggle_button action="my_live_component_action" target={@myself} value={@some_value}>
<span>Whatever you want</span>
</.toggle_button>
"""
def toggle_button(assigns) do
assigns = assigns |> assign_new(:id, fn -> assigns.action end)
~H"""
<label for={@id} class="inline-flex relative items-center cursor-pointer">
<input
id={@id}
type="checkbox"
value={@value}
checked={@value}
class="sr-only peer"
data-qa={@id}
{
if assigns |> Map.has_key?(:target),
do: %{"phx-click": @action, "phx-value-value": @value, "phx-target": @target},
else: %{"phx-click": @action, "phx-value-value": @value}
}
/>
<div class="w-11 h-6 bg-gray-300 rounded-full peer
peer-focus:ring-4 peer-focus:ring-teal-300 dark:peer-focus:ring-teal-800
peer-checked:bg-gray-600
peer-checked:after:translate-x-full peer-checked:after:border-white
after:content-[''] after:absolute after:top-1 after:left-[2px] after:bg-white after:border-gray-300
after:border after:rounded-full after:h-5 after:w-5
after:transition-all after:duration-250 after:ease-in-out
transition-colors duration-250 ease-in-out">
</div>
<span class="ml-3 text-sm font-medium text-gray-900 dark:text-gray-300">
<%= render_slot(@inner_block) %>
</span>
</label>
"""
end
end

View File

@ -9,14 +9,12 @@
action={Routes.user_registration_path(@conn, :create)}
class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center"
>
<div :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
<p>
<%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %>
</p>
</div>
<p :if={@changeset.action && not @changeset.valid?()} class="alert alert-danger col-span-3">
<%= dgettext("errors", "oops, something went wrong! please check the errors below.") %>
</p>
<%= if @invite do %>
<%= hidden_input(f, :invite_token, value: @invite.token) %>
<%= if @invite_token do %>
<%= hidden_input(f, :invite_token, value: @invite_token) %>
<% end %>
<%= label(f, :email, class: "title text-lg text-primary-400") %>