rename to cannery
This commit is contained in:
70
lib/cannery_web/components/invite_card.ex
Normal file
70
lib/cannery_web/components/invite_card.ex
Normal file
@ -0,0 +1,70 @@
|
||||
defmodule CanneryWeb.Components.InviteCard do
|
||||
@moduledoc """
|
||||
Display card for an invite
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
alias Cannery.Accounts.{Invite, Invites, User}
|
||||
alias CanneryWeb.Endpoint
|
||||
|
||||
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
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out">
|
||||
<h1 class="title text-xl">
|
||||
<%= @invite.name %>
|
||||
</h1>
|
||||
|
||||
<%= if @invite.disabled_at |> is_nil() do %>
|
||||
<h2 class="title text-md">
|
||||
<%= if @invite.uses_left do %>
|
||||
<%= gettext(
|
||||
"Uses Left: %{uses_left_count}",
|
||||
uses_left_count: @invite.uses_left
|
||||
) %>
|
||||
<% else %>
|
||||
<%= gettext("Uses Left: Unlimited") %>
|
||||
<% end %>
|
||||
</h2>
|
||||
<% else %>
|
||||
<h2 class="title text-md">
|
||||
<%= gettext("Invite Disabled") %>
|
||||
</h2>
|
||||
<% end %>
|
||||
|
||||
<.qr_code
|
||||
content={Routes.user_registration_url(Endpoint, :new, invite: @invite.token)}
|
||||
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}"}
|
||||
class="mx-2 my-1 text-xs px-4 py-2 rounded-lg text-center break-all text-gray-100 bg-primary-800"
|
||||
phx-no-format
|
||||
><%= Routes.user_registration_url(Endpoint, :new, invite: @invite.token) %></code>
|
||||
<%= render_slot(@code_actions) %>
|
||||
</div>
|
||||
|
||||
<div :if={@inner_block} class="flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
87
lib/cannery_web/components/table_component.ex
Normal file
87
lib/cannery_web/components/table_component.ex
Normal file
@ -0,0 +1,87 @@
|
||||
defmodule CanneryWeb.Components.TableComponent do
|
||||
@moduledoc """
|
||||
Livecomponent that presents a resortable table
|
||||
|
||||
It takes the following required assigns:
|
||||
- `:columns`: An array of maps containing the following keys
|
||||
- `:label`: A gettext'd or otherwise user-facing string label for the
|
||||
column. Can be nil
|
||||
- `:key`: A string key used for sorting
|
||||
- `:class`: Extra classes to be applied to the column element, if desired.
|
||||
Optional
|
||||
- `:sortable`: If false, will prevent the user from sorting with it.
|
||||
Optional
|
||||
- `:values`: An array of maps containing data for each row. Each map is
|
||||
string-keyed with the associated column key to the following values:
|
||||
- A single element, like string, integer or Phoenix.LiveView.Rendered
|
||||
object, like returned from the ~H sigil
|
||||
- A tuple, containing a custom value used for sorting, and the displayed
|
||||
content.
|
||||
"""
|
||||
|
||||
use CanneryWeb, :live_component
|
||||
alias Phoenix.LiveView.Socket
|
||||
|
||||
@impl true
|
||||
@spec update(
|
||||
%{
|
||||
required(:columns) =>
|
||||
list(%{
|
||||
required(:label) => String.t() | nil,
|
||||
required(:key) => String.t() | nil,
|
||||
optional(:class) => String.t(),
|
||||
optional(:sortable) => false
|
||||
}),
|
||||
required(:rows) =>
|
||||
list(%{
|
||||
(key :: String.t()) => any() | {custom_sort_value :: String.t(), value :: any()}
|
||||
}),
|
||||
optional(any()) => any()
|
||||
},
|
||||
Socket.t()
|
||||
) :: {:ok, Socket.t()}
|
||||
def update(%{columns: columns, rows: rows} = assigns, socket) do
|
||||
initial_key = columns |> List.first() |> Map.get(:key)
|
||||
rows = rows |> Enum.sort_by(fn row -> row |> Map.get(initial_key) end, :asc)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(assigns)
|
||||
|> assign(columns: columns, rows: rows, last_sort_key: initial_key, sort_mode: :asc)
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"sort_by",
|
||||
%{"sort-key" => key},
|
||||
%{assigns: %{rows: rows, last_sort_key: key, sort_mode: sort_mode}} = socket
|
||||
) do
|
||||
sort_mode = if sort_mode == :asc, do: :desc, else: :asc
|
||||
rows = rows |> sort_by_custom_sort_value_or_value(key, sort_mode)
|
||||
{:noreply, socket |> assign(sort_mode: sort_mode, rows: rows)}
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
"sort_by",
|
||||
%{"sort-key" => key},
|
||||
%{assigns: %{rows: rows}} = socket
|
||||
) do
|
||||
rows = rows |> sort_by_custom_sort_value_or_value(key, :asc)
|
||||
{:noreply, socket |> assign(last_sort_key: key, sort_mode: :asc, rows: rows)}
|
||||
end
|
||||
|
||||
defp sort_by_custom_sort_value_or_value(rows, key, sort_mode) do
|
||||
rows
|
||||
|> Enum.sort_by(
|
||||
fn row ->
|
||||
case row |> Map.get(key) do
|
||||
{custom_sort_key, _value} -> custom_sort_key
|
||||
value -> value
|
||||
end
|
||||
end,
|
||||
sort_mode
|
||||
)
|
||||
end
|
||||
end
|
48
lib/cannery_web/components/table_component.html.heex
Normal file
48
lib/cannery_web/components/table_component.html.heex
Normal file
@ -0,0 +1,48 @@
|
||||
<div class="w-full overflow-x-auto border border-gray-600 rounded-lg shadow-lg bg-black">
|
||||
<table class="min-w-full table-auto text-center bg-white">
|
||||
<thead class="border-b border-primary-600">
|
||||
<tr>
|
||||
<%= for %{key: key, label: label} = column <- @columns do %>
|
||||
<%= if column |> Map.get(:sortable, true) do %>
|
||||
<th class={["p-2", column[:class]]}>
|
||||
<span
|
||||
class="cursor-pointer"
|
||||
phx-click="sort_by"
|
||||
phx-value-sort-key={key}
|
||||
phx-target={@myself}
|
||||
>
|
||||
<span class="underline"><%= label %></span>
|
||||
<%= if @last_sort_key == key do %>
|
||||
<%= case @sort_mode do %>
|
||||
<% :asc -> %>
|
||||
<i class="fas fa-sm fa-chevron-down"></i>
|
||||
<% :desc -> %>
|
||||
<i class="fas fa-sm fa-chevron-up"></i>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<i class="fas fa-sm fa-chevron-up opacity-0"></i>
|
||||
<% end %>
|
||||
</span>
|
||||
</th>
|
||||
<% else %>
|
||||
<th class={["p-2", column[:class]]}>
|
||||
<%= label %>
|
||||
</th>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :for={values <- @rows}>
|
||||
<td :for={%{key: key} = value <- @columns} class={["p-2", value[:class]]}>
|
||||
<%= case values |> Map.get(key) do %>
|
||||
<% {_custom_sort_value, value} -> %>
|
||||
<%= value %>
|
||||
<% value -> %>
|
||||
<%= value %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
100
lib/cannery_web/components/topbar.ex
Normal file
100
lib/cannery_web/components/topbar.ex
Normal file
@ -0,0 +1,100 @@
|
||||
defmodule CanneryWeb.Components.Topbar do
|
||||
@moduledoc """
|
||||
Component that renders a topbar with user functions/links
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
|
||||
alias Cannery.Accounts
|
||||
alias CanneryWeb.{Endpoint, HomeLive}
|
||||
|
||||
def topbar(assigns) do
|
||||
assigns =
|
||||
%{results: [], title_content: nil, flash: nil, current_user: nil} |> Map.merge(assigns)
|
||||
|
||||
~H"""
|
||||
<nav role="navigation" class="mb-8 px-8 py-4 w-full bg-primary-400">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center">
|
||||
<div class="mb-4 sm:mb-0 sm:mr-8 flex flex-row justify-start items-center space-x-2">
|
||||
<.link
|
||||
navigate={Routes.live_path(Endpoint, HomeLive)}
|
||||
class="mx-2 my-1 leading-5 text-xl text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Cannery") %>
|
||||
</.link>
|
||||
|
||||
<%= if @title_content do %>
|
||||
<span class="mx-2 my-1">
|
||||
|
|
||||
</span>
|
||||
<%= @title_content %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<hr class="mb-2 sm:hidden hr-light" />
|
||||
|
||||
<ul class="flex flex-row flex-wrap justify-center items-center
|
||||
text-lg text-white text-ellipsis">
|
||||
<%= if @current_user do %>
|
||||
<li :if={@current_user.role == :admin} class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.invite_index_path(Endpoint, :index)}
|
||||
class="text-white text-white hover:underline"
|
||||
>
|
||||
<%= gettext("Invites") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.user_settings_path(Endpoint, :edit)}
|
||||
class="text-white text-white hover:underline truncate"
|
||||
>
|
||||
<%= @current_user.email %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
href={Routes.user_session_path(Endpoint, :delete)}
|
||||
method="delete"
|
||||
data-confirm={dgettext("prompts", "Are you sure you want to log out?")}
|
||||
>
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</.link>
|
||||
</li>
|
||||
<li
|
||||
:if={
|
||||
@current_user.role == :admin and function_exported?(Routes, :live_dashboard_path, 2)
|
||||
}
|
||||
class="mx-2 my-1"
|
||||
>
|
||||
<.link
|
||||
navigate={Routes.live_dashboard_path(Endpoint, :home)}
|
||||
class="text-white text-white hover:underline"
|
||||
>
|
||||
<i class="fas fa-gauge"></i>
|
||||
</.link>
|
||||
</li>
|
||||
<% else %>
|
||||
<li :if={Accounts.allow_registration?()} class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.user_registration_path(Endpoint, :new)}
|
||||
class="text-white text-white hover:underline truncate"
|
||||
>
|
||||
<%= dgettext("actions", "Register") %>
|
||||
</.link>
|
||||
</li>
|
||||
<li class="mx-2 my-1">
|
||||
<.link
|
||||
navigate={Routes.user_session_path(Endpoint, :new)}
|
||||
class="text-white text-white hover:underline truncate"
|
||||
>
|
||||
<%= dgettext("actions", "Log in") %>
|
||||
</.link>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
"""
|
||||
end
|
||||
end
|
48
lib/cannery_web/components/user_card.ex
Normal file
48
lib/cannery_web/components/user_card.ex
Normal file
@ -0,0 +1,48 @@
|
||||
defmodule CanneryWeb.Components.UserCard do
|
||||
@moduledoc """
|
||||
Display card for a user
|
||||
"""
|
||||
|
||||
use CanneryWeb, :component
|
||||
|
||||
def user_card(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
id={"user-#{@user.id}"}
|
||||
class="mx-4 my-2 px-8 py-4 flex flex-col justify-center items-center text-center
|
||||
border border-gray-400 rounded-lg shadow-lg hover:shadow-md
|
||||
transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<h1 class="px-4 py-2 rounded-lg title text-xl break-all">
|
||||
<%= @user.email %>
|
||||
</h1>
|
||||
|
||||
<h3 class="px-4 py-2 rounded-lg title text-lg">
|
||||
<p>
|
||||
<%= if @user.confirmed_at do %>
|
||||
<%= gettext(
|
||||
"User was confirmed at%{confirmed_datetime}",
|
||||
confirmed_datetime: ""
|
||||
) %>
|
||||
<.datetime datetime={@user.confirmed_at} />
|
||||
<% else %>
|
||||
<%= gettext("Email unconfirmed") %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<%= gettext(
|
||||
"User registered on%{registered_datetime}",
|
||||
registered_datetime: ""
|
||||
) %>
|
||||
<.datetime datetime={@user.inserted_at} />
|
||||
</p>
|
||||
</h3>
|
||||
|
||||
<div :if={@inner_block} class="px-4 py-2 flex space-x-4 justify-center items-center">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
Reference in New Issue
Block a user