refactored for new project name
This commit is contained in:
parent
0039146cd4
commit
82ab1d1ea5
113 changed files with 417 additions and 412 deletions
lib/shift73k_web/live
18
lib/shift73k_web/live/admin_dashboard_live.ex
Normal file
18
lib/shift73k_web/live/admin_dashboard_live.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Shift73kWeb.AdminDashboardLive do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket = assign_defaults(socket, session)
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<section class="phx-hero">
|
||||
<h1>Welcome to the admin dashboard!</h1>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
end
|
69
lib/shift73k_web/live/live_helpers.ex
Normal file
69
lib/shift73k_web/live/live_helpers.ex
Normal file
|
@ -0,0 +1,69 @@
|
|||
defmodule Shift73kWeb.LiveHelpers do
|
||||
import Phoenix.LiveView
|
||||
import Phoenix.LiveView.Helpers
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
alias Shift73kWeb.UserAuth
|
||||
|
||||
@doc """
|
||||
Performs the {:noreply, socket} for a given socket.
|
||||
This helps make the noreply pipeable
|
||||
"""
|
||||
def live_noreply(socket), do: {:noreply, socket}
|
||||
|
||||
@doc """
|
||||
Performs the {:ok, socket} for a given socket.
|
||||
This helps make the ok reply pipeable
|
||||
"""
|
||||
def live_okreply(socket), do: {:ok, socket}
|
||||
|
||||
@doc """
|
||||
Renders a component inside the `Shift73kWeb.ModalComponent` component.
|
||||
|
||||
The rendered modal receives a `:return_to` option to properly update
|
||||
the URL when the modal is closed.
|
||||
|
||||
## Examples
|
||||
|
||||
<%= live_modal @socket, Shift73kWeb.PropertyLive.FormComponent,
|
||||
id: @property.id || :new,
|
||||
action: @live_action,
|
||||
property: @property,
|
||||
return_to: Routes.property_index_path(@socket, :index) %>
|
||||
"""
|
||||
def live_modal(socket, component, opts) do
|
||||
modal_opts = [id: :modal, component: component, opts: opts]
|
||||
# dirty little workaround for elixir complaining about socket being unused
|
||||
_socket = socket
|
||||
live_component(socket, Shift73kWeb.ModalComponent, modal_opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Loads default assigns for liveviews
|
||||
"""
|
||||
def assign_defaults(socket, session) do
|
||||
Shift73kWeb.Endpoint.subscribe(UserAuth.pubsub_topic())
|
||||
assign_current_user(socket, session)
|
||||
end
|
||||
|
||||
# For liveviews, ensures current_user is in socket assigns.
|
||||
def assign_current_user(socket, session) do
|
||||
with user_token when not is_nil(user_token) <- session["user_token"],
|
||||
%User{} = user <- Accounts.get_user_by_session_token(user_token) do
|
||||
assign(socket, :current_user, user)
|
||||
else
|
||||
_ -> socket
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Copies current flash into new put_flash invocations.
|
||||
To be used before a push_patch.
|
||||
"""
|
||||
def copy_flash(%{assigns: %{flash: flash}} = socket) do
|
||||
Enum.reduce(flash, socket, fn {k, v}, acc ->
|
||||
put_flash(acc, String.to_existing_atom(k), v)
|
||||
end)
|
||||
end
|
||||
end
|
40
lib/shift73k_web/live/modal_component.ex
Normal file
40
lib/shift73k_web/live/modal_component.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
defmodule Shift73kWeb.ModalComponent do
|
||||
use Shift73kWeb, :live_component
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<div id="<%= @id %>" class="modal fade"
|
||||
phx-hook="BsModal"
|
||||
phx-window-keydown="hide"
|
||||
phx-key="escape"
|
||||
phx-target="#<%= @id %>"
|
||||
phx-page-loading>
|
||||
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><%= Keyword.get(@opts, :title, "Modal title") %></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
<%= live_component @socket, @component, @opts %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("close", _, socket) do
|
||||
send(self(), {:close_modal, true})
|
||||
live_noreply(socket)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("hide", _, socket) do
|
||||
{:noreply, push_event(socket, "modal-please-hide", %{})}
|
||||
end
|
||||
end
|
40
lib/shift73k_web/live/page_live.ex
Normal file
40
lib/shift73k_web/live/page_live.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
defmodule Shift73kWeb.PageLive do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket = assign_defaults(socket, session)
|
||||
{:ok, assign(socket, query: "", results: %{})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("suggest", %{"q" => query}, socket) do
|
||||
{:noreply, assign(socket, results: search(query), query: query)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("search", %{"q" => query}, socket) do
|
||||
case search(query) do
|
||||
%{^query => vsn} ->
|
||||
{:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}
|
||||
|
||||
_ ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, "No dependencies found matching \"#{query}\"")
|
||||
|> assign(results: %{}, query: query)}
|
||||
end
|
||||
end
|
||||
|
||||
defp search(query) do
|
||||
if not Shift73kWeb.Endpoint.config(:code_reloader) do
|
||||
raise "action disabled when not in development"
|
||||
end
|
||||
|
||||
for {app, desc, vsn} <- Application.started_applications(),
|
||||
app = to_string(app),
|
||||
String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
|
||||
into: %{},
|
||||
do: {app, vsn}
|
||||
end
|
||||
end
|
49
lib/shift73k_web/live/page_live.html.leex
Normal file
49
lib/shift73k_web/live/page_live.html.leex
Normal file
|
@ -0,0 +1,49 @@
|
|||
<section class="phx-hero">
|
||||
<h1><%= gettext "Welcome to %{name}!", name: "Phoenix" %></h1>
|
||||
<p>Peace of mind from prototype to production</p>
|
||||
|
||||
<form phx-change="suggest" phx-submit="search">
|
||||
<input type="text" name="q" value="<%= @query %>" placeholder="Live dependency search" list="results" autocomplete="off"/>
|
||||
<datalist id="results">
|
||||
<%= for {app, _vsn} <- @results do %>
|
||||
<option value="<%= app %>"><%= app %></option>
|
||||
<% end %>
|
||||
</datalist>
|
||||
<button type="submit" phx-disable-with="Searching...">Go to Hexdocs</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="row align-items-start">
|
||||
<article class="col">
|
||||
<h2>Resources</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://hexdocs.pm/phoenix/overview.html">Guides & Docs</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/phoenixframework/phoenix">Source</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/phoenixframework/phoenix/blob/v1.5/CHANGELOG.md">v1.5 Changelog</a>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="col">
|
||||
<h2>Help</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://elixirforum.com/c/phoenix-forum">Forum</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://webchat.freenode.net/?channels=elixir-lang">#elixir-lang on Freenode IRC</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/elixirphoenix">Twitter @elixirphoenix</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://elixir-slackin.herokuapp.com/">Elixir on Slack</a>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
</section>
|
66
lib/shift73k_web/live/property_live/form_component.ex
Normal file
66
lib/shift73k_web/live/property_live/form_component.ex
Normal file
|
@ -0,0 +1,66 @@
|
|||
defmodule Shift73kWeb.PropertyLive.FormComponent do
|
||||
use Shift73kWeb, :live_component
|
||||
|
||||
alias Shift73k.Properties
|
||||
|
||||
@impl true
|
||||
def update(%{property: property} = assigns, socket) do
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(:changeset, Properties.change_property(property))
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"property" => property_params}, socket) do
|
||||
changeset =
|
||||
socket.assigns.property
|
||||
|> Properties.change_property(property_params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"property" => property_params}, socket) do
|
||||
save_property(socket, socket.assigns.action, property_params)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel", _, socket) do
|
||||
{:noreply, push_event(socket, "modal-please-hide", %{})}
|
||||
end
|
||||
|
||||
defp save_property(socket, :edit, property_params) do
|
||||
case Properties.update_property(socket.assigns.property, property_params) do
|
||||
{:ok, _property} ->
|
||||
flash = {:info, "Property updated successfully"}
|
||||
send(self(), {:put_flash_message, flash})
|
||||
|
||||
socket
|
||||
|> push_event("modal-please-hide", %{})
|
||||
|> live_noreply()
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, :changeset, changeset)}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_property(socket, :new, property_params) do
|
||||
current_user = socket.assigns.current_user
|
||||
property_params = Map.put(property_params, "user_id", current_user.id)
|
||||
|
||||
case Properties.create_property(property_params) do
|
||||
{:ok, _property} ->
|
||||
flash = {:info, "Property created successfully"}
|
||||
send(self(), {:put_flash_message, flash})
|
||||
|
||||
socket
|
||||
|> push_event("modal-please-hide", %{})
|
||||
|> live_noreply()
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
{:noreply, assign(socket, changeset: changeset)}
|
||||
end
|
||||
end
|
||||
end
|
57
lib/shift73k_web/live/property_live/form_component.html.leex
Normal file
57
lib/shift73k_web/live/property_live/form_component.html.leex
Normal file
|
@ -0,0 +1,57 @@
|
|||
<%= form_for @changeset, "#", [
|
||||
id: "property-form",
|
||||
phx_target: @myself,
|
||||
phx_change: "validate",
|
||||
phx_submit: "save"
|
||||
], fn f -> %>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :name)%>">
|
||||
<%= label f, :name, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<%= text_input f, :name,
|
||||
class: input_class(f, :name, "form-control"),
|
||||
aria_describedby: error_id(f, :name)
|
||||
%>
|
||||
<%= error_tag f, :name %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :price)%>">
|
||||
<%= label f, :price, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<%= number_input f, :price,
|
||||
class: input_class(f, :price, "form-control"),
|
||||
step: "any",
|
||||
aria_describedby: error_id(f, :price)
|
||||
%>
|
||||
<%= error_tag f, :price %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :description)%>">
|
||||
<%= label f, :description, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<%= textarea f, :description,
|
||||
class: input_class(f, :description, "form-control"),
|
||||
aria_describedby: error_id(f, :description)
|
||||
%>
|
||||
<%= error_tag f, :description %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<%= link "Cancel", to: "#", class: "btn btn-secondary me-2", phx_click: "cancel", phx_target: @myself %>
|
||||
<%= submit "Save",
|
||||
class: "btn btn-primary ",
|
||||
disabled: !@changeset.valid?,
|
||||
aria_disabled: !@changeset.valid? && "true" || false,
|
||||
phx_disable_with: "Saving..."
|
||||
%>
|
||||
|
||||
</div>
|
||||
|
||||
<% end %>
|
90
lib/shift73k_web/live/property_live/index.ex
Normal file
90
lib/shift73k_web/live/property_live/index.ex
Normal file
|
@ -0,0 +1,90 @@
|
|||
defmodule Shift73kWeb.PropertyLive.Index do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
alias Shift73k.Properties
|
||||
alias Shift73k.Properties.Property
|
||||
alias Shift73kWeb.Roles
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket = assign_defaults(socket, session)
|
||||
{:ok, assign(socket, :properties, [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
live_action = socket.assigns.live_action
|
||||
property = property_from_params(params)
|
||||
|
||||
if Roles.can?(current_user, property, live_action) do
|
||||
socket
|
||||
|> assign(:properties, list_properties())
|
||||
|> assign(:modal_return_to, Routes.property_index_path(socket, :index))
|
||||
|> apply_action(live_action, params)
|
||||
|> live_noreply()
|
||||
else
|
||||
socket
|
||||
|> put_flash(:error, "Unauthorised")
|
||||
|> redirect(to: "/")
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(:page_title, "Edit Property")
|
||||
|> assign(:property, Properties.get_property!(id))
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "New Property")
|
||||
|> assign(:property, %Property{})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "Listing Properties")
|
||||
|> assign(:property, nil)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
property = Properties.get_property!(id)
|
||||
|
||||
if Shift73kWeb.Roles.can?(current_user, property, :delete) do
|
||||
property = Properties.get_property!(id)
|
||||
{:ok, _} = Properties.delete_property(property)
|
||||
|
||||
{:noreply, assign(socket, :properties, list_properties())}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, "Unauthorised")
|
||||
|> redirect(to: "/")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do
|
||||
socket |> copy_flash() |> push_patch(to: to) |> live_noreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||
end
|
||||
|
||||
defp property_from_params(params)
|
||||
|
||||
defp property_from_params(%{"id" => id}),
|
||||
do: Properties.get_property!(id)
|
||||
|
||||
defp property_from_params(_params), do: %Property{}
|
||||
|
||||
defp list_properties do
|
||||
Properties.list_properties()
|
||||
end
|
||||
end
|
46
lib/shift73k_web/live/property_live/index.html.leex
Normal file
46
lib/shift73k_web/live/property_live/index.html.leex
Normal file
|
@ -0,0 +1,46 @@
|
|||
<%= if @live_action in [:new, :edit] do %>
|
||||
<%= live_modal @socket, Shift73kWeb.PropertyLive.FormComponent,
|
||||
id: @property.id || :new,
|
||||
title: @page_title,
|
||||
action: @live_action,
|
||||
property: @property,
|
||||
current_user: @current_user %>
|
||||
<% end %>
|
||||
|
||||
<div class="d-flex justify-content-between d-flex align-items-end">
|
||||
<h2>Listing Properties</h2>
|
||||
<span><%= live_patch "New Property", to: Routes.property_index_path(@socket, :new), class: "btn btn-primary" %></span>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Price</th>
|
||||
<th scope="col">Description</th>
|
||||
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="properties">
|
||||
<%= for property <- @properties do %>
|
||||
<tr id="property-<%= property.id %>">
|
||||
<td><%= property.name %></td>
|
||||
<td><%= property.price %></td>
|
||||
<td><%= property.description %></td>
|
||||
|
||||
<td>
|
||||
<%= if Roles.can?(@current_user, property, :show) do %>
|
||||
<%= live_redirect "Show", to: Routes.property_show_path(@socket, :show, property), class: "link-secondary mx-1" %>
|
||||
<% end %>
|
||||
<%= if Roles.can?(@current_user, property, :edit) do %>
|
||||
<%= live_patch "Edit", to: Routes.property_index_path(@socket, :edit, property), class: "mx-1" %>
|
||||
<% end %>
|
||||
<%= if Roles.can?(@current_user, property, :delete) do %>
|
||||
<%= link "Delete", to: "#", phx_click: "delete", phx_value_id: property.id, data: [confirm: "Are you sure?"], class: "link-danger mx-1" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
45
lib/shift73k_web/live/property_live/show.ex
Normal file
45
lib/shift73k_web/live/property_live/show.ex
Normal file
|
@ -0,0 +1,45 @@
|
|||
defmodule Shift73kWeb.PropertyLive.Show do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
alias Shift73k.Properties
|
||||
alias Shift73kWeb.Roles
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket = assign_defaults(socket, session)
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(%{"id" => id}, _, socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
live_action = socket.assigns.live_action
|
||||
property = Properties.get_property!(id)
|
||||
|
||||
if Roles.can?(current_user, property, live_action) do
|
||||
socket
|
||||
|> assign(:property, property)
|
||||
|> assign(:page_title, page_title(live_action))
|
||||
|> assign(:modal_return_to, Routes.property_show_path(socket, :show, property))
|
||||
|> live_noreply()
|
||||
else
|
||||
socket
|
||||
|> put_flash(:error, "Unauthorised")
|
||||
|> redirect(to: "/")
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do
|
||||
socket |> copy_flash() |> push_patch(to: to) |> live_noreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||
end
|
||||
|
||||
defp page_title(:show), do: "Show Property"
|
||||
defp page_title(:edit), do: "Edit Property"
|
||||
end
|
33
lib/shift73k_web/live/property_live/show.html.leex
Normal file
33
lib/shift73k_web/live/property_live/show.html.leex
Normal file
|
@ -0,0 +1,33 @@
|
|||
<h2>Show Property</h2>
|
||||
|
||||
<%= if @live_action in [:edit] do %>
|
||||
<%= live_modal @socket, Shift73kWeb.PropertyLive.FormComponent,
|
||||
id: @property.id,
|
||||
title: @page_title,
|
||||
action: @live_action,
|
||||
property: @property %>
|
||||
<% end %>
|
||||
|
||||
<table class="table table-nonfluid">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="text-end">Name</th>
|
||||
<td><%= @property.name %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-end">Price</th>
|
||||
<td><%= @property.price %></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-end">Description</th>
|
||||
<td><%= @property.description %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%= if Roles.can?(@current_user, @property, :index) do %>
|
||||
<%= live_redirect "Back", to: Routes.property_index_path(@socket, :index), class: "btn btn-secondary" %>
|
||||
<% end %>
|
||||
<%= if Roles.can?(@current_user, @property, :edit) do %>
|
||||
<%= live_patch "Edit", to: Routes.property_show_path(@socket, :edit, @property), class: "btn btn-primary" %>
|
||||
<% end %>
|
61
lib/shift73k_web/live/user/registration.ex
Normal file
61
lib/shift73k_web/live/user/registration.ex
Normal file
|
@ -0,0 +1,61 @@
|
|||
defmodule Shift73kWeb.UserLive.Registration do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket
|
||||
|> assign_defaults(session)
|
||||
|> assign(page_title: "Register")
|
||||
|> assign(changeset: Accounts.change_user_registration(%User{}))
|
||||
|> assign(login_params: init_login_params(session))
|
||||
|> assign(trigger_submit: false)
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
defp init_login_params(session) do
|
||||
%{
|
||||
user_id: nil,
|
||||
user_return_to: Map.get(session, "user_return_to", "/"),
|
||||
messages: [
|
||||
success: "Welcome! Your new account has been created, and you've been logged in.",
|
||||
info:
|
||||
"Some features may be unavailable until you confirm your email address. Check your inbox for instructions."
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||
cs = Accounts.change_user_registration(%User{}, user_params)
|
||||
{:noreply, assign(socket, changeset: %{cs | action: :validate})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
user_params
|
||||
|> Map.put("role", Accounts.registration_role())
|
||||
|> Accounts.register_user()
|
||||
|> case do
|
||||
{:ok, user} ->
|
||||
{:ok, %Bamboo.Email{}} =
|
||||
Accounts.deliver_user_confirmation_instructions(
|
||||
user,
|
||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||
)
|
||||
|
||||
socket
|
||||
|> assign(login_params: %{socket.assigns.login_params | user_id: user.id})
|
||||
|> assign(trigger_submit: true)
|
||||
|> live_noreply()
|
||||
|
||||
{:error, cs} ->
|
||||
socket
|
||||
|> put_flash(:error, "Ope — registration failed for some reason.")
|
||||
|> assign(:changeset, cs)
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
end
|
70
lib/shift73k_web/live/user/registration.html.leex
Normal file
70
lib/shift73k_web/live/user/registration.html.leex
Normal file
|
@ -0,0 +1,70 @@
|
|||
<div class="row justify-content-center">
|
||||
<div class="col-sm-9 col-md-7 col-lg-5 col-xl-4 ">
|
||||
|
||||
<h2>
|
||||
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline"] %>
|
||||
Register
|
||||
</h2>
|
||||
<p class="lead">Registration gains additional features, like remembering your song request history.</p>
|
||||
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "reg_form"], fn f -> %>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
||||
<%= label f, :email, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-at", [class: "icon"] %>
|
||||
</span>
|
||||
<%= email_input f, :email,
|
||||
value: input_value(f, :email),
|
||||
class: input_class(f, :email, "form-control"),
|
||||
placeholder: "e.g., babka@73k.us",
|
||||
maxlength: User.max_email,
|
||||
autofocus: true,
|
||||
phx_debounce: "blur",
|
||||
aria_describedby: error_id(f, :email)
|
||||
%>
|
||||
<%= error_tag f, :email %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
||||
<%= label f, :password, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :password,
|
||||
value: input_value(f, :password),
|
||||
class: input_class(f, :password, "form-control"),
|
||||
maxlength: User.max_password,
|
||||
phx_debounce: "250",
|
||||
aria_describedby: error_id(f, :password)
|
||||
%>
|
||||
<%= error_tag f, :password %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= submit (@trigger_submit && "Saving..." || "Register"),
|
||||
class: "btn btn-primary",
|
||||
disabled: @trigger_submit || !@changeset.valid?,
|
||||
aria_disabled: (@trigger_submit || !@changeset.valid?) && "true" || false,
|
||||
phx_disable_with: "Saving..."
|
||||
%>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
<p>
|
||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %> |
|
||||
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@socket, :new) %>
|
||||
</p>
|
||||
|
||||
<%# hidden form for initial login after registration %>
|
||||
<%= form_for :user, Routes.user_session_path(@socket, :create), [phx_trigger_action: @trigger_submit, id: "reg_trigger"], fn f -> %>
|
||||
<%= hidden_input f, :params_token, value: Phoenix.Token.encrypt(Shift73kWeb.Endpoint, "login_params", @login_params) %>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
</div>
|
40
lib/shift73k_web/live/user/reset_password.ex
Normal file
40
lib/shift73k_web/live/user/reset_password.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
defmodule Shift73kWeb.UserLive.ResetPassword do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
user = Accounts.get_user!(session["user_id"])
|
||||
|
||||
socket
|
||||
|> assign_defaults(session)
|
||||
|> assign(page_title: "Reset password")
|
||||
|> assign(changeset: Accounts.change_user_password(user))
|
||||
|> assign(user: user)
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||
cs = Accounts.change_user_password(socket.assigns.user, user_params)
|
||||
{:noreply, socket |> assign(changeset: %{cs | action: :validate})}
|
||||
end
|
||||
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
case Accounts.reset_user_password(socket.assigns.user, user_params) do
|
||||
{:ok, _} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:info, "Password reset successfully.")
|
||||
|> redirect(to: Routes.user_session_path(socket, :new))}
|
||||
|
||||
{:error, changeset} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(:error, "Please check the errors below.")
|
||||
|> assign(changeset: changeset)}
|
||||
end
|
||||
end
|
||||
end
|
62
lib/shift73k_web/live/user/reset_password.html.leex
Normal file
62
lib/shift73k_web/live/user/reset_password.html.leex
Normal file
|
@ -0,0 +1,62 @@
|
|||
<div class="row justify-content-center">
|
||||
<div class="col-sm-9 col-md-7 col-lg-5 col-xl-4 ">
|
||||
|
||||
<h2>
|
||||
<%= icon_div @socket, "bi-shield-lock", [class: "icon baseline"] %>
|
||||
Reset password
|
||||
</h2>
|
||||
<p class="lead">Hi <%= @user.email %> — What new word of passage will confirm you are who you say you are?</p>
|
||||
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, novalidate: true, id: "pw_reset_form"], fn f -> %>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
||||
<%= label f, :password, "New password", class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :password,
|
||||
value: input_value(f, :password),
|
||||
class: input_class(f, :password, "form-control"),
|
||||
maxlength: User.max_password,
|
||||
autofocus: true,
|
||||
aria_describedby: error_id(f, :password)
|
||||
%>
|
||||
<%= error_tag f, :password %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :password_confirmation) %>">
|
||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-key-fill", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :password_confirmation,
|
||||
value: input_value(f, :password_confirmation),
|
||||
class: input_class(f, :password_confirmation, "form-control"),
|
||||
maxlength: User.max_password,
|
||||
aria_describedby: error_id(f, :password_confirmation)
|
||||
%>
|
||||
<%= error_tag f, :password_confirmation %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= submit "Reset password",
|
||||
class: "btn btn-primary",
|
||||
disabled: !@changeset.valid?,
|
||||
aria_disabled: !@changeset.valid? && "true" || false,
|
||||
phx_disable_with: "Saving..."
|
||||
%>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
<p class="mt-3 is-pulled-right">
|
||||
<%= link "Register", to: Routes.user_registration_path(@socket, :new) %> |
|
||||
<%= link "Log in", to: Routes.user_session_path(@socket, :new) %>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
39
lib/shift73k_web/live/user/settings.ex
Normal file
39
lib/shift73k_web/live/user/settings.ex
Normal file
|
@ -0,0 +1,39 @@
|
|||
defmodule Shift73kWeb.UserLive.Settings do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket
|
||||
|> assign_defaults(session)
|
||||
|> alert_email_verified?()
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
defp alert_email_verified?(socket) do
|
||||
case socket.assigns.current_user do
|
||||
%{confirmed_at: nil} ->
|
||||
put_flash(socket, :warning, [
|
||||
"Your email hasn't been confirmed, some areas may be restricted. Shall we ",
|
||||
link("resend the verification email?",
|
||||
to: Routes.user_confirmation_path(socket, :new),
|
||||
class: "alert-link"
|
||||
)
|
||||
])
|
||||
|
||||
_ ->
|
||||
socket
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:clear_flash_message, flash_type}, socket) do
|
||||
socket |> clear_flash(flash_type) |> live_noreply()
|
||||
end
|
||||
end
|
9
lib/shift73k_web/live/user/settings.html.leex
Normal file
9
lib/shift73k_web/live/user/settings.html.leex
Normal file
|
@ -0,0 +1,9 @@
|
|||
<h2 class="mb-3">
|
||||
<%= icon_div @socket, "bi-sliders", [class: "icon baseline"] %>
|
||||
User Settings
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.Email, id: "email-#{@current_user.id}", current_user: @current_user %>
|
||||
<%= live_component @socket, Shift73kWeb.UserLive.Settings.Password, id: "password-#{@current_user.id}", current_user: @current_user %>
|
||||
</div>
|
62
lib/shift73k_web/live/user/settings/email.ex
Normal file
62
lib/shift73k_web/live/user/settings/email.ex
Normal file
|
@ -0,0 +1,62 @@
|
|||
defmodule Shift73kWeb.UserLive.Settings.Email do
|
||||
use Shift73kWeb, :live_component
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
@impl true
|
||||
def update(%{current_user: user} = assigns, socket) do
|
||||
socket
|
||||
|> assign(id: assigns.id)
|
||||
|> assign(current_user: user)
|
||||
|> assign(changeset: get_changeset(user))
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
defp get_changeset(user, user_params \\ %{}) do
|
||||
Accounts.change_user_email(user, user_params)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||
cs = get_changeset(socket.assigns.current_user, user_params)
|
||||
{:noreply, assign(socket, changeset: %{cs | action: :validate})}
|
||||
end
|
||||
|
||||
# user_settings_path GET /users/settings/confirm_email/:token Shift73kWeb.UserSettingsController :confirm_email
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
case Accounts.apply_user_email(socket.assigns.current_user, user_params) do
|
||||
{:ok, applied_user} ->
|
||||
Accounts.deliver_update_email_instructions(
|
||||
applied_user,
|
||||
socket.assigns.current_user.email,
|
||||
&Routes.user_settings_url(socket, :confirm_email, &1)
|
||||
)
|
||||
|
||||
send(self(), {:clear_flash_message, :error})
|
||||
|
||||
send(
|
||||
self(),
|
||||
{:put_flash_message,
|
||||
{:info, "A link to confirm your e-mail change has been sent to the new address."}}
|
||||
)
|
||||
|
||||
socket
|
||||
|> assign(changeset: get_changeset(socket.assigns.current_user))
|
||||
|> live_noreply()
|
||||
|
||||
{:error, cs} ->
|
||||
cu = socket.assigns.current_user
|
||||
cpw = user_params["current_password"]
|
||||
valid_password? = User.valid_password?(cu, cpw)
|
||||
msg = (valid_password? && "Could not reset email.") || "Invalid current password."
|
||||
send(self(), {:put_flash_message, {:error, msg}})
|
||||
|
||||
socket
|
||||
|> assign(changeset: cs)
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
end
|
52
lib/shift73k_web/live/user/settings/email.html.leex
Normal file
52
lib/shift73k_web/live/user/settings/email.html.leex
Normal file
|
@ -0,0 +1,52 @@
|
|||
<div id="<%= @id %>" class="col-sm-9 col-md-7 col-lg-5 col-xl-4 mt-1">
|
||||
|
||||
<h3>Change email</h3>
|
||||
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :email) %>">
|
||||
<%= label f, :email, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-at", [class: "icon"] %>
|
||||
</span>
|
||||
<%= email_input f, :email,
|
||||
value: input_value(f, :email),
|
||||
class: input_class(f, :email, "form-control"),
|
||||
placeholder: "e.g., babka@73k.us",
|
||||
maxlength: User.max_email,
|
||||
phx_debounce: "500",
|
||||
aria_describedby: error_id(f, :email)
|
||||
%>
|
||||
<%= error_tag f, :email %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :current_password) %>">
|
||||
<%= label f, :current_password, class: "form-label" %>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-lock", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :current_password,
|
||||
value: input_value(f, :current_password),
|
||||
class: "form-control",
|
||||
aria_describedby: error_id(f, :current_password)
|
||||
%>
|
||||
<%= error_tag f, :current_password %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= submit "Change email",
|
||||
class: "btn btn-primary",
|
||||
disabled: !@changeset.valid? || input_value(f, :current_password) == "",
|
||||
aria_disabled: (!@changeset.valid? || input_value(f, :current_password) == "") && "true" || false,
|
||||
phx_disable_with: "Saving..."
|
||||
%>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
</div>
|
57
lib/shift73k_web/live/user/settings/password.ex
Normal file
57
lib/shift73k_web/live/user/settings/password.ex
Normal file
|
@ -0,0 +1,57 @@
|
|||
defmodule Shift73kWeb.UserLive.Settings.Password do
|
||||
use Shift73kWeb, :live_component
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
|
||||
@impl true
|
||||
def update(%{current_user: user} = assigns, socket) do
|
||||
socket
|
||||
|> assign(id: assigns.id)
|
||||
|> assign(current_user: user)
|
||||
|> assign(changeset: get_changeset(user))
|
||||
|> assign(login_params: init_login_params(socket))
|
||||
|> assign(trigger_submit: false)
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
defp get_changeset(user, user_params \\ %{}) do
|
||||
Accounts.change_user_password(user, user_params)
|
||||
end
|
||||
|
||||
defp init_login_params(socket) do
|
||||
%{
|
||||
user_id: nil,
|
||||
user_return_to: Routes.user_settings_path(socket, :edit),
|
||||
messages: [info: "Password updated successfully."]
|
||||
}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||
cs = get_changeset(socket.assigns.current_user, user_params)
|
||||
{:noreply, assign(socket, changeset: %{cs | action: :validate})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
case Accounts.update_user_password(socket.assigns.current_user, user_params) do
|
||||
{:ok, user} ->
|
||||
socket
|
||||
|> assign(login_params: %{socket.assigns.login_params | user_id: user.id})
|
||||
|> assign(trigger_submit: true)
|
||||
|> live_noreply()
|
||||
|
||||
{:error, cs} ->
|
||||
cu = socket.assigns.current_user
|
||||
cpw = user_params["current_password"]
|
||||
valid_password? = User.valid_password?(cu, cpw)
|
||||
msg = (valid_password? && "Could not change password.") || "Invalid current password."
|
||||
send(self(), {:put_flash_message, {:error, msg}})
|
||||
|
||||
socket
|
||||
|> assign(changeset: cs)
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
end
|
77
lib/shift73k_web/live/user/settings/password.html.leex
Normal file
77
lib/shift73k_web/live/user/settings/password.html.leex
Normal file
|
@ -0,0 +1,77 @@
|
|||
<div id="<%= @id %>" class="col-sm-9 col-md-7 col-lg-5 col-xl-4 mt-1">
|
||||
|
||||
<h3>Change password</h3>
|
||||
|
||||
<%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, phx_target: @myself], fn f -> %>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :password) %>">
|
||||
<%= label f, :password, "New password", class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :password,
|
||||
value: input_value(f, :password),
|
||||
class: input_class(f, :password, "form-control"),
|
||||
maxlength: User.max_password,
|
||||
phx_debounce: "500",
|
||||
aria_describedby: error_id(f, :password)
|
||||
%>
|
||||
<%= error_tag f, :password %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :password_confirmation) %>">
|
||||
<%= label f, :password_confirmation, "Confirm new password", class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-key-fill", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :password_confirmation,
|
||||
value: input_value(f, :password_confirmation),
|
||||
class: input_class(f, :password_confirmation, "form-control"),
|
||||
maxlength: User.max_password,
|
||||
aria_describedby: error_id(f, :password_confirmation)
|
||||
%>
|
||||
<%= error_tag f, :password_confirmation %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :current_password) %>">
|
||||
<%= label f, :current_password, class: "form-label" %>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-lock", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :current_password,
|
||||
value: input_value(f, :current_password),
|
||||
class: "form-control",
|
||||
aria_describedby: error_id(f, :current_password)
|
||||
%>
|
||||
<%= error_tag f, :current_password %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<%= submit "Change password",
|
||||
class: "btn btn-primary",
|
||||
disabled: !@changeset.valid? || input_value(f, :current_password) == "",
|
||||
aria_disabled: (!@changeset.valid? || input_value(f, :current_password) == "") && "true" || false,
|
||||
phx_disable_with: "Saving..."
|
||||
%>
|
||||
</div>
|
||||
|
||||
<% end %>
|
||||
|
||||
<%# hidden form for initial login after registration %>
|
||||
<%= form_for :user, Routes.user_session_path(@socket, :create), [phx_trigger_action: @trigger_submit, id: "settings_pw_change_trigger"], fn f -> %>
|
||||
<%= hidden_input f, :params_token, value: Phoenix.Token.encrypt(Shift73kWeb.Endpoint, "login_params", @login_params) %>
|
||||
<% end %>
|
||||
|
||||
<%# hidden form to submit user for relogin after password change %>
|
||||
<%#= form_for :user_login, Routes.user_session_path(@socket, :create), [phx_trigger_action: @trigger_submit], fn f -> %>
|
||||
<%#= hidden_input f, :login_params_token, value: Phoenix.Token.encrypt(Shift73kWeb.Endpoint, "login_params", @login_params) %>
|
||||
<%#= hidden_input f, :remember_me, value: false %>
|
||||
<%# end %>
|
||||
|
||||
</div>
|
18
lib/shift73k_web/live/user_dashboard_live.ex
Normal file
18
lib/shift73k_web/live/user_dashboard_live.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defmodule Shift73kWeb.UserDashboardLive do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket = assign_defaults(socket, session)
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~L"""
|
||||
<section class="phx-hero">
|
||||
<h2>Welcome to the user dashboard!</h2>
|
||||
</section>
|
||||
"""
|
||||
end
|
||||
end
|
44
lib/shift73k_web/live/user_management/delete_component.ex
Normal file
44
lib/shift73k_web/live/user_management/delete_component.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defmodule Shift73kWeb.UserManagement.DeleteComponent do
|
||||
use Shift73kWeb, :live_component
|
||||
|
||||
alias Shift73k.Accounts
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel", _, socket) do
|
||||
{:noreply, push_event(socket, "modal-please-hide", %{})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("confirm", %{"id" => id, "email" => email}, socket) do
|
||||
id
|
||||
|> Accounts.get_user()
|
||||
|> Accounts.delete_user()
|
||||
|> case do
|
||||
{:ok, _} ->
|
||||
flash = {:info, "User deleted successfully: \"#{email}\""}
|
||||
send(self(), {:put_flash_message, flash})
|
||||
|
||||
socket
|
||||
|> push_event("modal-please-hide", %{})
|
||||
|> live_noreply()
|
||||
|
||||
{:error, _} ->
|
||||
flash =
|
||||
{:error,
|
||||
"Some error trying to delete user \"#{email}\". Possibly already deleted? Reloading list..."}
|
||||
|
||||
send(self(), {:put_flash_message, flash})
|
||||
|
||||
socket
|
||||
|> push_event("modal-please-hide", %{})
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
<div class="modal-body">
|
||||
|
||||
Are you sure you want to delete "<%= @delete_user.email %>"?
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<%= link "Cancel", to: "#", class: "btn btn-secondary me-2", phx_click: "cancel", phx_target: @myself %>
|
||||
<%= link "Confirm Delete", to: "#",
|
||||
class: "btn btn-danger",
|
||||
phx_click: "confirm",
|
||||
phx_target: @myself,
|
||||
phx_value_id: @delete_user.id,
|
||||
phx_value_email: @delete_user.email %>
|
||||
|
||||
</div>
|
95
lib/shift73k_web/live/user_management/form_component.ex
Normal file
95
lib/shift73k_web/live/user_management/form_component.ex
Normal file
|
@ -0,0 +1,95 @@
|
|||
defmodule Shift73kWeb.UserManagement.FormComponent do
|
||||
use Shift73kWeb, :live_component
|
||||
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
alias Shift73kWeb.Roles
|
||||
|
||||
@impl true
|
||||
def update(assigns, socket) do
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> init_changeset(assigns)
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
defp init_changeset(socket, %{action: :new, user: user}) do
|
||||
params = %{role: Accounts.registration_role()}
|
||||
assign(socket, changeset: Accounts.change_user_registration(user, params))
|
||||
end
|
||||
|
||||
defp init_changeset(socket, %{action: :edit, user: user}) do
|
||||
assign(socket, changeset: Accounts.change_user_update(user))
|
||||
end
|
||||
|
||||
defp validate_changes(%{action: :new, user: user}, user_params) do
|
||||
Accounts.change_user_registration(user, user_params)
|
||||
end
|
||||
|
||||
defp validate_changes(%{action: :edit, user: user}, user_params) do
|
||||
Accounts.change_user_update(user, user_params)
|
||||
end
|
||||
|
||||
defp save_user(%{assigns: %{action: :new}} = socket, user_params) do
|
||||
case Accounts.register_user(user_params) do
|
||||
{:ok, user} ->
|
||||
{:ok, %Bamboo.Email{}} =
|
||||
Accounts.deliver_user_confirmation_instructions(
|
||||
user,
|
||||
&Routes.user_confirmation_url(socket, :confirm, &1)
|
||||
)
|
||||
|
||||
flash = {:info, "User created successfully: #{user.email}"}
|
||||
send(self(), {:put_flash_message, flash})
|
||||
|
||||
socket
|
||||
|> push_event("modal-please-hide", %{})
|
||||
|> live_noreply()
|
||||
|
||||
{:error, cs} ->
|
||||
socket
|
||||
|> put_flash(:error, "Some error creating this user...")
|
||||
|> assign(changeset: cs)
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
|
||||
defp save_user(%{assigns: %{action: :edit, user: user}} = socket, user_params) do
|
||||
case Accounts.update_user(user, user_params) do
|
||||
{:ok, user} ->
|
||||
flash = {:info, "User updated successfully: #{user.email}"}
|
||||
send(self(), {:put_flash_message, flash})
|
||||
|
||||
socket
|
||||
|> push_event("modal-please-hide", %{})
|
||||
|> live_noreply()
|
||||
|
||||
{:error, cs} ->
|
||||
{:noreply, assign(socket, :changeset, cs)}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("validate", %{"user" => user_params}, socket) do
|
||||
cs = validate_changes(socket.assigns, user_params)
|
||||
{:noreply, assign(socket, :changeset, %{cs | action: :validate})}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("save", %{"user" => user_params}, socket) do
|
||||
save_user(socket, user_params)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("cancel", _, socket) do
|
||||
{:noreply, push_event(socket, "modal-please-hide", %{})}
|
||||
end
|
||||
|
||||
def role_description(role) when is_atom(role) do
|
||||
Keyword.get(User.roles(), role)
|
||||
end
|
||||
|
||||
def role_description(role) when is_binary(role) do
|
||||
Keyword.get(User.roles(), String.to_existing_atom(role))
|
||||
end
|
||||
end
|
|
@ -0,0 +1,72 @@
|
|||
<%= form_for @changeset, "#", [
|
||||
phx_target: @myself,
|
||||
phx_change: "validate",
|
||||
phx_submit: "save"
|
||||
], fn f -> %>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="mb-3" phx-feedback-for="<%= input_id(f, :email)%>">
|
||||
<%= label f, :email, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-at", [class: "icon"] %>
|
||||
</span>
|
||||
<%= email_input f, :email,
|
||||
value: input_value(f, :email),
|
||||
class: input_class(f, :email, "form-control"),
|
||||
placeholder: "e.g., babka@73k.us",
|
||||
maxlength: User.max_email,
|
||||
autofocus: true,
|
||||
phx_debounce: "250",
|
||||
aria_describedby: error_id(f, :email)
|
||||
%>
|
||||
<%= error_tag f, :email %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if Roles.can?(@current_user, %User{}, :edit_role) do %>
|
||||
<%= label f, :role, class: "form-label" %>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-shield-shaded", [class: "icon"] %>
|
||||
</span>
|
||||
<%= select f, :role, Enum.map(User.roles(), fn {k, _v} -> {String.capitalize(Atom.to_string(k)), k} end), class: "form-select" %>
|
||||
<span class="valid-feedback text-primary" style="display: block;">
|
||||
<%= role_description(input_value(f, :role)) %>
|
||||
</span>
|
||||
</div>
|
||||
<% else %>
|
||||
<%= hidden_input f, :role, value: input_value(f, :role) %>
|
||||
<% end %>
|
||||
|
||||
<div phx-feedback-for="<%= input_id(f, :password) %>">
|
||||
<%= label f, :password, class: "form-label" %>
|
||||
<div class="input-group has-validation">
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-key", [class: "icon"] %>
|
||||
</span>
|
||||
<%= password_input f, :password,
|
||||
value: input_value(f, :password),
|
||||
class: input_class(f, :password, "form-control"),
|
||||
maxlength: User.max_password,
|
||||
aria_describedby: error_id(f, :password)
|
||||
%>
|
||||
<%= error_tag f, :password %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
<%= link "Cancel", to: "#", class: "btn btn-secondary", phx_click: "cancel", phx_target: @myself %>
|
||||
<%= submit "Save",
|
||||
class: "btn btn-primary ",
|
||||
disabled: !@changeset.valid?,
|
||||
aria_disabled: !@changeset.valid? && "true" || false,
|
||||
phx_disable_with: "Saving..."
|
||||
%>
|
||||
|
||||
</div>
|
||||
|
||||
<% end %>
|
191
lib/shift73k_web/live/user_management/index.ex
Normal file
191
lib/shift73k_web/live/user_management/index.ex
Normal file
|
@ -0,0 +1,191 @@
|
|||
defmodule Shift73kWeb.UserManagementLive.Index do
|
||||
use Shift73kWeb, :live_view
|
||||
|
||||
import Ecto.Query
|
||||
import Shift73kWeb.Pagination
|
||||
import Shift73k.Util.Dt
|
||||
|
||||
alias Shift73k.Repo
|
||||
alias Shift73k.Accounts
|
||||
alias Shift73k.Accounts.User
|
||||
alias Shift73kWeb.Roles
|
||||
|
||||
@impl true
|
||||
def mount(_params, session, socket) do
|
||||
socket
|
||||
|> assign_defaults(session)
|
||||
|> assign(:page, nil)
|
||||
|> live_okreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
current_user = socket.assigns.current_user
|
||||
live_action = socket.assigns.live_action
|
||||
user = user_from_params(params)
|
||||
|
||||
if Roles.can?(current_user, user, live_action) do
|
||||
socket
|
||||
|> assign(:query, query_map(params))
|
||||
|> assign_modal_return_to()
|
||||
|> assign(:delete_user, nil)
|
||||
|> assign(:page, nil)
|
||||
|> request_page_query()
|
||||
|> apply_action(socket.assigns.live_action, params)
|
||||
|> live_noreply()
|
||||
else
|
||||
socket
|
||||
|> put_flash(:error, "Unauthorised")
|
||||
|> redirect(to: "/")
|
||||
|> live_noreply()
|
||||
end
|
||||
end
|
||||
|
||||
defp request_page_query(%{assigns: %{query: query}} = socket) do
|
||||
send(self(), {:do_page_query, query})
|
||||
socket
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "Listing Users")
|
||||
|> assign(:user, nil)
|
||||
end
|
||||
|
||||
defp apply_action(socket, :new, _params) do
|
||||
socket
|
||||
|> assign(:page_title, "New User")
|
||||
|> assign(:user, %User{})
|
||||
end
|
||||
|
||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||
socket
|
||||
|> assign(:page_title, "Edit User")
|
||||
|> assign(:user, Accounts.get_user!(id))
|
||||
end
|
||||
|
||||
def assign_modal_return_to(%{assigns: %{query: query}} = socket) do
|
||||
to = Routes.user_management_index_path(socket, :index, Enum.into(query, []))
|
||||
assign(socket, :modal_return_to, to)
|
||||
end
|
||||
|
||||
defp user_from_params(params)
|
||||
|
||||
defp user_from_params(%{"id" => id}),
|
||||
do: Accounts.get_user!(id)
|
||||
|
||||
defp user_from_params(_params), do: %User{}
|
||||
|
||||
def query_map(params) do
|
||||
%{
|
||||
filter: params["filter"] || "",
|
||||
sort_by: (params["sort_by"] in ~w(email inserted_at role) && params["sort_by"]) || "email",
|
||||
sort_order: (params["sort_order"] == "desc" && "desc") || "asc",
|
||||
page_number: String.to_integer(params["page_number"] || "1"),
|
||||
page_size: String.to_integer(params["page_size"] || "10")
|
||||
}
|
||||
end
|
||||
|
||||
defp page_query(query) do
|
||||
from(u in User)
|
||||
|> or_where([u], ilike(u.email, ^"%#{query.filter}%"))
|
||||
|> order_by([u], [
|
||||
{^String.to_existing_atom(query.sort_order), ^String.to_existing_atom(query.sort_by)}
|
||||
])
|
||||
|> Repo.paginate(page: query.page_number, page_size: query.page_size)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("delete-modal", %{"id" => id}, socket) do
|
||||
{:noreply, assign(socket, :delete_user, Accounts.get_user(id))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("filter-change", params, socket) do
|
||||
send(self(), {:query_update, Map.put(params, "page_number", "1")})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("filter-clear", _params, socket) do
|
||||
send(self(), {:query_update, %{"filter" => "", "page_number" => "1"}})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"sort-change",
|
||||
%{"sort_by" => column} = params,
|
||||
%{assigns: %{query: query}} = socket
|
||||
) do
|
||||
(column == query.sort_by &&
|
||||
send(
|
||||
self(),
|
||||
{:query_update, %{"sort_order" => (query.sort_order == "asc" && "desc") || "asc"}}
|
||||
)) ||
|
||||
send(self(), {:query_update, Map.put(params, "sort_order", "asc")})
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("sort-by-change", %{"sort" => params}, socket) do
|
||||
send(self(), {:query_update, params})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("sort-order-change", _params, socket) do
|
||||
new_sort_order = (socket.assigns.query.sort_order == "asc" && "desc") || "asc"
|
||||
send(self(), {:query_update, %{"sort_order" => new_sort_order}})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("page-change", params, socket) do
|
||||
send(self(), {:query_update, params})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("page-size-change", params, socket) do
|
||||
send(self(), {:query_update, Map.put(params, "page_number", "1")})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:query_update, params}, %{assigns: %{query: q}} = socket) do
|
||||
{:noreply,
|
||||
push_patch(socket,
|
||||
to: Routes.user_management_index_path(socket, :index, get_new_params(params, q)),
|
||||
replace: true
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:do_page_query, query}, socket) do
|
||||
{:noreply, assign(socket, :page, page_query(query))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:close_modal, _}, %{assigns: %{modal_return_to: to}} = socket) do
|
||||
socket |> copy_flash() |> push_patch(to: to) |> live_noreply()
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:put_flash_message, {flash_type, msg}}, socket) do
|
||||
socket |> put_flash(flash_type, msg) |> live_noreply()
|
||||
end
|
||||
|
||||
defp get_new_params(params, query) do
|
||||
[
|
||||
{:filter, Map.get(params, "filter") || query.filter},
|
||||
{:sort_by, Map.get(params, "sort_by") || query.sort_by},
|
||||
{:sort_order, Map.get(params, "sort_order") || query.sort_order},
|
||||
{:page_number, Map.get(params, "page_number") || query.page_number},
|
||||
{:page_size, Map.get(params, "page_size") || query.page_size}
|
||||
]
|
||||
end
|
||||
|
||||
def dt_out(ndt), do: format_ndt(ndt, "{YYYY} {Mshort} {0D}, {h12}:{0m} {am}")
|
||||
end
|
216
lib/shift73k_web/live/user_management/index.html.leex
Normal file
216
lib/shift73k_web/live/user_management/index.html.leex
Normal file
|
@ -0,0 +1,216 @@
|
|||
<%= if @live_action in [:new, :edit] do %>
|
||||
<%= live_modal @socket, Shift73kWeb.UserManagement.FormComponent,
|
||||
id: @user.id || :new,
|
||||
title: @page_title,
|
||||
action: @live_action,
|
||||
user: @user,
|
||||
current_user: @current_user %>
|
||||
<% end %>
|
||||
|
||||
<%= if @delete_user do %>
|
||||
<%= live_modal @socket, Shift73kWeb.UserManagement.DeleteComponent,
|
||||
id: @delete_user.id,
|
||||
title: "Delete User",
|
||||
delete_user: @delete_user,
|
||||
current_user: @current_user %>
|
||||
<% end %>
|
||||
|
||||
|
||||
<h2 class="mb-3">
|
||||
<%= icon_div @socket, "bi-people", [class: "icon baseline"] %>
|
||||
Listing Users
|
||||
</h2>
|
||||
|
||||
<%# filtering and new item creation %>
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between d-flex align-items-start mb-3">
|
||||
|
||||
<%= live_patch to: Routes.user_management_index_path(@socket, :new, Enum.into(@query, [])),
|
||||
class: "btn btn-primary mb-3 mb-sm-0" do %>
|
||||
<%= icon_div @socket, "bi-person-plus", [class: "icon baseline me-1"] %>
|
||||
New User
|
||||
<% end %>
|
||||
|
||||
<%= form_for :filter, "#", [phx_change: "filter-change"], fn flt -> %>
|
||||
<div class="input-group">
|
||||
|
||||
<span class="input-group-text">
|
||||
<%= icon_div @socket, "bi-filter", [class: "icon"] %>
|
||||
</span>
|
||||
|
||||
<%= text_input flt, :filter, name: "filter", class: "form-control", placeholder: "Filter users...", value: @query.filter %>
|
||||
|
||||
</button>
|
||||
|
||||
<%= if @query.filter == "" do %>
|
||||
<button class="btn btn-outline-secondary" type="button" aria-label="Clear filter" aria-disabled="true" disabled>
|
||||
<% else %>
|
||||
<button class="btn btn-outline-secondary" type="button" aria-label="Clear filter" phx-click="filter-clear">
|
||||
<% end %>
|
||||
<%= icon_div @socket, "bi-backspace", [class: "icon baseline"], [role: "img", aria_hidden: false] %>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
|
||||
<%# main data table %>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="email" class="cursor-pointer">
|
||||
Email
|
||||
<%= if @query.sort_by == "email", do: icon_div @socket,
|
||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||
[class: "icon baseline ms-1"]
|
||||
%>
|
||||
</th>
|
||||
|
||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="role" class="cursor-pointer">
|
||||
Role
|
||||
<%= if @query.sort_by == "role", do: icon_div @socket,
|
||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||
[class: "icon baseline ms-1"]
|
||||
%>
|
||||
</th>
|
||||
|
||||
<th scope="col" phx-click="sort-change" phx-value-sort_by="inserted_at" class="cursor-pointer">
|
||||
Created at
|
||||
<%= if @query.sort_by == "inserted_at", do: icon_div @socket,
|
||||
(@query.sort_order == "desc" && "bi-sort-up-alt" || "bi-sort-down-alt"),
|
||||
[class: "icon baseline ms-1"]
|
||||
%>
|
||||
</th>
|
||||
|
||||
<th scope="col">Confirmed?</th>
|
||||
|
||||
<th></th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody id="users">
|
||||
<%= if !@page do %>
|
||||
<tr>
|
||||
<td class="text-center" colspan="5">
|
||||
<div class="spinner-border text-primary my-5" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% else %>
|
||||
<%= for user <- @page.entries do %>
|
||||
<tr id="user-<%= user.id %>">
|
||||
<td class="align-middle"><%= user.email %></td>
|
||||
<td class="align-middle"><%= user.role |> Atom.to_string() |> String.capitalize() %></td>
|
||||
<td class="align-middle" style="white-space: nowrap;"><%= dt_out(user.inserted_at) %></td>
|
||||
<td class="align-middle">
|
||||
<%= if user.confirmed_at do %>
|
||||
<span class="visually-hidden">Yes</span>
|
||||
<%= icon_div @socket, "bi-check", [class: "icon baseline fs-4 text-success"], [role: "img", aria_hidden: false] %>
|
||||
<% else %>
|
||||
<span class="visually-hidden">No</span>
|
||||
<%= icon_div @socket, "bi-x", [class: "icon baseline fs-4 text-warning"], [role: "img", aria_hidden: false] %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="align-middle text-end text-nowrap">
|
||||
|
||||
<%= live_patch to: Routes.user_management_index_path(@socket, :edit, user.id, Enum.into(@query, [])), class: "btn btn-outline-primary btn-sm text-nowrap" do %>
|
||||
<%= icon_div @socket, "bi-pencil", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
|
||||
Edit
|
||||
<% end %>
|
||||
|
||||
<%= if Roles.can?(@current_user, user, :delete) do %>
|
||||
|
||||
<button class="btn btn-outline-danger btn-sm text-nowrap" phx-click="delete-modal" phx-value-id="<%= user.id %>">
|
||||
<%= icon_div @socket, "bi-trash", [class: "icon baseline", style: "margin-right:0.125rem;"] %>
|
||||
Delete
|
||||
</button>
|
||||
|
||||
<% end %>
|
||||
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<%# pagination interface %>
|
||||
<%= if @page do %>
|
||||
<div class="d-flex flex-column flex-sm-row justify-content-between d-flex align-items-start my-3">
|
||||
|
||||
<%# <div class="d-flex justify-content-between d-flex align-items-start"> %>
|
||||
|
||||
<%# items per page selector %>
|
||||
<div class="d-flex align-items-center mb-3 mb-sm-0">
|
||||
<%= form_for :page_size, "#", [phx_change: "page-size-change"], fn pgsz -> %>
|
||||
<%= select pgsz, :page_size,
|
||||
[10, 15, 20, 30, 50, 100] |> Enum.map(fn n -> {"#{n} per page", n} end),
|
||||
value: @query.page_size,
|
||||
id: "table_page_size_page_size",
|
||||
name: "page_size",
|
||||
class: "form-select"
|
||||
%>
|
||||
<% end %>
|
||||
<span class="ms-2"><%= @page.total_entries %> found</span>
|
||||
</div>
|
||||
|
||||
<%# main pagination %>
|
||||
<nav aria-label="User list page navigation">
|
||||
<ul class="pagination mb-0">
|
||||
|
||||
<%# previous page button %>
|
||||
<% icon = icon_div @socket, "bi-chevron-left", [class: "icon baseline"] %>
|
||||
<%= if @page.page_number == 1 do %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
<% else %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" aria-label="Previous" phx-value-page_number="<%= @page.page_number - 1 %>" phx-click="page-change"><%= icon %></a>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<%# page buttons %>
|
||||
<%= for page_num <- generate_page_list(@page.page_number, @page.total_pages) do %>
|
||||
<%= cond do %>
|
||||
<%= page_num < 1 -> %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">…</span>
|
||||
<span class="visually-hidden" role="img" aria-label="ellipses">…</span>
|
||||
</li>
|
||||
<% page_num == @page.page_number -> %>
|
||||
<li class="page-item active" aria-current="page">
|
||||
<span class="page-link"><%= page_num %></a>
|
||||
</li>
|
||||
<% true -> %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" phx-value-page_number="<%= page_num %>" phx-click="page-change"><%= page_num %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%# next page button %>
|
||||
<% icon = icon_div @socket, "bi-chevron-right", [class: "icon baseline"] %>
|
||||
<%= if @page.page_number == @page.total_pages do %>
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true"><%= icon %></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
<% else %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="#" aria-label="Next" phx-value-page_number="<%= @page.page_number + 1 %>" phx-click="page-change"><%= icon %></a>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
<% end %>
|
Loading…
Add table
Add a link
Reference in a new issue