Add pagination tests for users and events in Admin module

This commit is contained in:
Alex Lion
2026-04-06 16:48:46 +02:00
parent 3fc4213b7c
commit 8ce74c06a2
16 changed files with 878 additions and 1016 deletions

View File

@@ -3,6 +3,19 @@ defmodule Claper.Accounts.User do
import Ecto.Changeset
@derive {
Flop.Schema,
max_limit: 100,
default_limit: 20,
pagination_types: [:page],
filterable: [:email, :role_id],
sortable: [:email, :role_id, :confirmed_at, :inserted_at],
default_order: %{
order_by: [:inserted_at],
order_directions: [:desc]
}
}
@type t :: %__MODULE__{
id: integer(),
uuid: Ecto.UUID.t(),

View File

@@ -316,128 +316,46 @@ defmodule Claper.Admin do
end
@doc """
Returns a paginated list of users.
Returns a paginated list of users using Flop.
## Options
* `:page` - The page number (default: 1)
* `:per_page` - The number of users per page (default: 20)
* `:search` - Search term for filtering users by email
* `:role` - Filter users by role name
Accepts Flop params for filtering, sorting, and pagination.
See `Claper.Accounts.User` Flop.Schema config for available fields.
## Examples
iex> list_users_paginated(%{page: 1, per_page: 10})
%{entries: [%User{}, ...], page_number: 1, page_size: 10, total_entries: 20, total_pages: 2}
iex> list_users_paginated(%{"page" => 1, "page_size" => 10})
{[%User{}, ...], %Flop.Meta{}}
"""
def list_users_paginated(params \\ %{}) do
page = Map.get(params, "page", 1)
per_page = Map.get(params, "per_page", 20)
search = Map.get(params, "search", "")
role = Map.get(params, "role", "")
query =
User
|> where([u], is_nil(u.deleted_at))
|> preload(:role)
|> join(:left, [u], r in assoc(u, :role), as: :role)
query =
if search != "" do
query |> where([u], ilike(u.email, ^"%#{search}%"))
else
query
end
query =
if role != "" do
query |> join(:inner, [u], r in assoc(u, :role), on: r.name == ^role)
else
query
end
query = query |> order_by([u], desc: u.inserted_at)
Repo.paginate(query, page: page, page_size: per_page)
Flop.validate_and_run!(query, params, for: User, replace_invalid_params: true)
end
@doc """
Returns a paginated list of events.
Returns a paginated list of events using Flop.
## Options
* `:page` - The page number (default: 1)
* `:per_page` - The number of events per page (default: 20)
* `:search` - Search term for filtering events by name
* `:status` - Filter events by status (upcoming, past)
* `:start_date` - Filter events by start date
* `:end_date` - Filter events by end date
* `:creator_id` - Filter events by creator ID
Accepts Flop params for filtering, sorting, and pagination.
See `Claper.Events.Event` Flop.Schema config for available fields.
## Examples
iex> list_events_paginated(%{page: 1, per_page: 10})
%{entries: [%Event{}, ...], page_number: 1, page_size: 10, total_entries: 20, total_pages: 2}
iex> list_events_paginated(%{"page" => 1, "page_size" => 10})
{[%Event{}, ...], %Flop.Meta{}}
"""
def list_events_paginated(params \\ %{}) do
page = Map.get(params, "page", 1)
per_page = Map.get(params, "per_page", 20)
search = Map.get(params, "search", "")
status = Map.get(params, "status", "")
start_date = Map.get(params, "start_date", nil)
end_date = Map.get(params, "end_date", nil)
creator_id = Map.get(params, "creator_id", nil)
query =
Event
|> join(:left, [e], u in assoc(e, :user), as: :user)
|> preload(:user)
query =
if search != "" do
query |> where([e], ilike(e.name, ^"%#{search}%"))
else
query
end
query =
case status do
"upcoming" ->
now = NaiveDateTime.utc_now()
query |> where([e], e.started_at > ^now)
"past" ->
now = NaiveDateTime.utc_now()
query |> where([e], e.started_at <= ^now)
_ ->
query
end
query =
if start_date do
query |> where([e], e.started_at >= ^start_date)
else
query
end
query =
if end_date do
query |> where([e], e.started_at <= ^end_date)
else
query
end
query =
if creator_id do
query |> where([e], e.user_id == ^creator_id)
else
query
end
query = query |> order_by([e], desc: e.started_at)
Repo.paginate(query, page: page, page_size: per_page)
Flop.validate_and_run!(query, params, for: Event, replace_invalid_params: true)
end
@doc """

View File

@@ -5,6 +5,8 @@ defmodule Claper.Audit.Log do
@derive {
Flop.Schema,
max_limit: 100,
default_limit: 20,
pagination_types: [:page],
filterable: [:action, :user_email],
sortable: [:inserted_at, :action],
default_order: %{

View File

@@ -2,6 +2,28 @@ defmodule Claper.Events.Event do
use Ecto.Schema
import Ecto.Changeset
@derive {
Flop.Schema,
max_limit: 100,
default_limit: 20,
pagination_types: [:page],
filterable: [:name, :code, :user_email],
sortable: [:name, :code, :started_at, :expired_at, :audience_peak],
default_order: %{
order_by: [:started_at],
order_directions: [:desc]
},
adapter_opts: [
join_fields: [
user_email: [
binding: :user,
field: :email,
path: [:user, :email]
]
]
]
}
@type t :: %__MODULE__{
id: integer(),
uuid: Ecto.UUID.t(),

View File

@@ -495,4 +495,84 @@ defmodule ClaperWeb.CoreComponents do
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
@doc """
Renders a DaisyUI pagination component from a Flop.Meta struct.
## Attributes
* `meta` - A `Flop.Meta` struct with pagination info.
* `path` - The base path for pagination links.
"""
attr :meta, :map, required: true
attr :path, :string, required: true
def pagination(assigns) do
meta = assigns.meta
assigns =
assigns
|> assign(:current_page, meta.current_page || 1)
|> assign(:total_pages, meta.total_pages || 1)
|> assign(:has_previous?, meta.has_previous_page?)
|> assign(:has_next?, meta.has_next_page?)
|> assign(:page_range, page_range(meta.current_page || 1, meta.total_pages || 1))
~H"""
<nav :if={@total_pages > 1} class="flex items-center gap-3" aria-label="Pagination">
<span class="text-sm text-base-content/60">
{gettext("Page %{current} of %{total}", current: @current_page, total: @total_pages)}
</span>
<div class="join">
<.link
patch={page_path(@path, @meta, @current_page - 1)}
class={[
"join-item btn btn-ghost btn-sm",
!@has_previous? && "btn-disabled"
]}
aria-label={gettext("Previous page")}
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</.link>
<.link
:for={page <- @page_range}
patch={page_path(@path, @meta, page)}
class={[
"join-item btn btn-sm",
if(page == @current_page, do: "btn-active", else: "btn-ghost")
]}
>
{page}
</.link>
<.link
patch={page_path(@path, @meta, @current_page + 1)}
class={[
"join-item btn btn-ghost btn-sm",
!@has_next? && "btn-disabled"
]}
aria-label={gettext("Next page")}
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</.link>
</div>
</nav>
"""
end
defp page_path(path, meta, page) do
Flop.Phoenix.build_path(path, %{meta | flop: %{meta.flop | page: page}})
end
defp page_range(_current, total) when total <= 5, do: 1..total
defp page_range(current, total) do
start = max(1, current - 2)
stop = min(total, start + 4)
start = max(1, stop - 4)
start..stop
end
end

View File

@@ -1,49 +1,22 @@
<h1 class="text-3xl font-bold mb-24">{@page_title}</h1>
<h1 class="text-3xl font-bold mb-6">{@page_title}</h1>
<%= if @live_action == :index do %>
<div class="flex flex-col sm:flex-row items-stretch gap-3 @container/controls">
<.form
for={@form}
class="grow flex flex-col sm:flex-row gap-3 sm:[&>div]:basis-[250px]"
phx-change="filter-logs"
>
<Flop.Phoenix.filter_fields :let={i} form={@form} fields={@fields}>
<ClaperWeb.CoreComponents.input field={i.field} label={i.label} type={i.type} {i.rest} />
</Flop.Phoenix.filter_fields>
</.form>
<Flop.Phoenix.pagination
meta={@meta}
path={~p"/admin/audit_logs"}
class="flex [&>ul]:mr-3 justify-end"
page_list_attrs={[class: "hidden @4xl/controls:flex"]}
page_list_item_attrs={[
class:
"grid place-content-stretch min-h-10 min-w-10 border-t border-r border-b border-gray-300 first:border-l first:rounded-l-full last:rounded-r-full first:[&>*]:rounded-l-full last:[&>*]:rounded-r-full text-gray-500 bg-white [&>*]:grid [&>*]:place-content-center"
]}
page_link_attrs={[
class: "hover:text-black hover:bg-gray-200"
]}
current_page_link_attrs={[
class: "pointer-events-none font-bold text-black"
]}
>
<:previous attrs={[
class:
"order-1 border border-gray-300 rounded-l-full h-10 p-5 bg-white flex items-center justify-center aria-disabled:text-gray-300 hover:not-aria-disabled:bg-gray-200"
]}>
{gettext("Previous")}
</:previous>
<:next attrs={[
class:
"order-2 border-t border-r border-b border-gray-300 rounded-r-full h-10 p-5 bg-white flex items-center justify-center aria-disabled:text-gray-300 hover:not-aria-disabled:bg-gray-200"
]}>
{gettext("Next")}
</:next>
<:ellipsis>
<span aria-hidden="true" class="text-gray-300">•••</span>
</:ellipsis>
</Flop.Phoenix.pagination>
</div>
<.form
for={@form}
class="flex flex-col sm:flex-row gap-3 sm:[&>div]:basis-[250px]"
phx-change="filter-logs"
>
<Flop.Phoenix.filter_fields :let={i} form={@form} fields={@fields}>
<ClaperWeb.CoreComponents.input
field={i.field}
label={i.label}
type={i.type}
phx-debounce="300"
{i.rest}
/>
</Flop.Phoenix.filter_fields>
</.form>
<Flop.Phoenix.table
items={@logs}
meta={@meta}
@@ -91,6 +64,10 @@
</.link>
</:action>
</Flop.Phoenix.table>
<div class="flex justify-end mt-4">
<ClaperWeb.CoreComponents.pagination meta={@meta} path={~p"/admin/audit_logs"} />
</div>
<% end %>
<%= if @live_action == :show do %>

View File

@@ -3,7 +3,6 @@ defmodule ClaperWeb.AdminLive.EventLive do
alias Claper.Admin
alias Claper.Events.Event
alias ClaperWeb.Helpers.CSVExporter
@impl true
def mount(_params, session, socket) do
@@ -11,12 +10,7 @@ defmodule ClaperWeb.AdminLive.EventLive do
Gettext.put_locale(ClaperWeb.Gettext, locale)
end
{:ok,
socket
|> assign(:page_title, gettext("Events"))
|> assign(:events, list_events())
|> assign(:search, "")
|> assign(:current_sort, %{field: :na, order: :asc})}
{:ok, socket}
end
@impl true
@@ -24,10 +18,25 @@ defmodule ClaperWeb.AdminLive.EventLive do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
defp apply_action(socket, :index, params) do
{events, meta} = Admin.list_events_paginated(params)
socket
|> assign(:page_title, "Events")
|> assign(:page_title, gettext("Events"))
|> assign(:event, nil)
|> assign(:events, events)
|> assign(:meta, meta)
|> assign(:form, Phoenix.Component.to_form(meta))
|> assign_new(:fields, fn ->
[
name: [
label: nil,
placeholder: gettext("Search events..."),
type: "text",
op: :ilike_or
]
]
end)
end
defp apply_action(socket, :new, _params) do
@@ -56,120 +65,14 @@ defmodule ClaperWeb.AdminLive.EventLive do
{:noreply,
socket
|> put_flash(:info, gettext("Event deleted successfully"))
|> assign(:events, list_events())}
|> push_patch(to: Flop.Phoenix.build_path(~p"/admin/events", socket.assigns.meta.flop))}
end
@impl true
def handle_event("search", %{"search" => search}, socket) do
events = search_events(search)
{:noreply, socket |> assign(:search, search) |> assign(:events, events)}
end
def handle_event("filter-events", unsigned_params, socket) do
flop = Flop.validate!(unsigned_params, for: Event, replace_invalid_params: true)
to = Flop.Phoenix.build_path(~p"/admin/events", flop)
@impl true
def handle_event("sort", %{"field" => field}, socket) do
field = String.to_existing_atom(field)
current_sort = socket.assigns.current_sort
direction =
if current_sort.field == field && current_sort.order == :asc, do: :desc, else: :asc
events = sort_events(socket.assigns.events, field, direction)
current_sort = %{field: field, order: direction}
{:noreply,
socket
|> assign(:events, events)
|> assign(:current_sort, current_sort)}
end
@impl true
def handle_info({:export_csv_requested, _params}, socket) do
filename = CSVExporter.generate_filename("events")
csv_content = CSVExporter.export_events_to_csv(socket.assigns.events)
{:noreply,
socket
|> put_flash(:info, gettext("Events exported successfully"))
|> push_event("download_csv", %{filename: filename, content: csv_content})}
end
@impl true
def handle_info({:table_action, action, event, _event_id}, socket) do
case action do
:view ->
{:noreply, push_navigate(socket, to: ~p"/admin/events/#{event}")}
:edit ->
{:noreply, push_navigate(socket, to: ~p"/admin/events/#{event}/edit")}
:delete ->
{:ok, _} = Claper.Events.delete_event(event)
{:noreply,
socket
|> put_flash(:info, gettext("Event deleted successfully"))
|> assign(:events, list_events())}
end
end
defp list_events do
Admin.list_all_events()
end
defp search_events(search) when search == "", do: list_events()
defp search_events(search) do
Admin.list_all_events(%{"search" => search})
end
defp sort_events(events, field, order) do
Enum.sort_by(events, &Map.get(&1, field), order)
end
def sort_indicator(assigns) do
~H"""
<%= if @current_sort.field == @field do %>
<%= if @current_sort.order == :asc do %>
<svg
class="ml-2 h-5 w-5 text-gray-500 group-hover:text-gray-700"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
<% else %>
<svg
class="ml-2 h-5 w-5 text-gray-500 group-hover:text-gray-700"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M14.707 12.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l2.293-2.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
<% end %>
<% else %>
<svg
class="ml-2 h-5 w-5 text-gray-400 opacity-0 group-hover:opacity-100"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
<% end %>
"""
{:noreply, push_patch(socket, to: to)}
end
end

View File

@@ -1,249 +1,95 @@
<%= case @live_action do %>
<% :index -> %>
<div phx-hook="CSVDownloader" id="event-list">
<div>
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">{gettext("Events")}</h1>
<div class="flex space-x-3">
<%!-- <.link navigate={~p"/admin/events/new"} class="btn btn-primary btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
class="w-3 h-3"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M12 4v16m8-8H4"
/>
</svg>
{gettext("New event")}
</.link> --%>
</div>
</div>
</div>
<h1 class="text-3xl font-bold mb-6">{gettext("Events")}</h1>
<div class="mt-4">
<div class="py-4">
<!-- Search Bar -->
<div class="mb-6">
<form phx-change="search" class="flex w-full md:w-1/2 my-3">
<label class="input">
<svg
class="h-[1em] opacity-50"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input
type="search"
name="search"
value={@search}
placeholder={gettext("Search events...")}
/>
</label>
</form>
<.form
for={@form}
class="flex flex-col sm:flex-row gap-3 sm:[&>div]:basis-[250px]"
phx-change="filter-events"
>
<Flop.Phoenix.filter_fields :let={i} form={@form} fields={@fields}>
<ClaperWeb.CoreComponents.input
field={i.field}
label={i.label}
type={i.type}
phx-debounce="300"
{i.rest}
/>
</Flop.Phoenix.filter_fields>
</.form>
<Flop.Phoenix.table
items={@events}
meta={@meta}
path={~p"/admin/events"}
opts={[
container: true,
container_attrs: [class: "card bg-base-100 shadow-xl overflow-x-auto my-5"],
no_results_content: ~H"""
<p class="text-center py-10">{gettext("No events found")}</p>
""",
table_attrs: [class: "table table-zebra w-full"],
symbol_asc: ~H"""
<span aria-hidden="true"><ClaperWeb.Icons.arrow_up /></span>
""",
symbol_desc: ~H"""
<span aria-hidden="true"><ClaperWeb.Icons.arrow_down /></span>
"""
]}
>
<:col :let={event} label={gettext("Name")} field={:name}>{event.name}</:col>
<:col :let={event} label={gettext("Code")} field={:code}>
<span class="uppercase">{event.code}</span>
</:col>
<:col :let={event} label={gettext("Owner")}>
{if event.user, do: event.user.email, else: gettext("No owner")}
</:col>
<:col :let={event} label={gettext("Started At")} field={:started_at}>
{if event.started_at,
do: Calendar.strftime(event.started_at, "%Y-%m-%d %H:%M"),
else: gettext("Not started")}
</:col>
<:col :let={event} label={gettext("Expired At")} field={:expired_at}>
{if event.expired_at,
do: Calendar.strftime(event.expired_at, "%Y-%m-%d %H:%M"),
else: gettext("Not expired")}
</:col>
<:col :let={event} label={gettext("Audience Peak")} field={:audience_peak}>
<div class="badge badge-outline">{event.audience_peak || 0}</div>
</:col>
<:action :let={event} tbody_td_attrs={[class: "text-right"]}>
<div class="flex justify-end gap-2">
<.link
navigate={~p"/admin/events/#{event}"}
class="btn btn-link btn-sm"
title={gettext("View event")}
>
<ClaperWeb.Icons.eye />
</.link>
<.link
navigate={~p"/admin/events/#{event}/edit"}
class="btn btn-link btn-sm text-primary"
title={gettext("Edit event")}
>
<span class="hero-pencil-square w-5 h-5" />
</.link>
<a
href="#"
phx-click="delete"
phx-value-id={event.id}
data-confirm={gettext("Are you sure you want to delete this event?")}
class="btn btn-link btn-sm text-error"
title={gettext("Delete event")}
>
<span class="hero-trash w-5 h-5" />
</a>
</div>
<!-- events Table -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-0">
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="name"
class="btn btn-ghost btn-sm"
>
{gettext("Name")} {sort_indicator(assigns |> Map.put(:field, :name))}
</button>
</th>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="code"
class="btn btn-ghost btn-sm"
>
{gettext("Code")} {sort_indicator(assigns |> Map.put(:field, :code))}
</button>
</th>
<th class="text-left">
{gettext("Owner")}
</th>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="started_at"
class="btn btn-ghost btn-sm"
>
{gettext("Started At")} {sort_indicator(
assigns
|> Map.put(:field, :started_at)
)}
</button>
</th>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="expired_at"
class="btn btn-ghost btn-sm"
>
{gettext("Expired At")} {sort_indicator(
assigns
|> Map.put(:field, :expired_at)
)}
</button>
</th>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="audience_peak"
class="btn btn-ghost btn-sm"
>
{gettext("Audience Peak")} {sort_indicator(
assigns
|> Map.put(:field, :audience_peak)
)}
</button>
</th>
<th class="text-right">
<span class="sr-only">{gettext("Actions")}</span>
</th>
</tr>
</thead>
<tbody>
<%= if Enum.empty?(@events) do %>
<tr>
<td colspan="7" class="text-center">
{gettext("No events found")}
</td>
</tr>
<% else %>
<%= for event <- @events do %>
<tr id={"event-#{event.id}"}>
<td class="font-medium">
{event.name}
</td>
<td class="uppercase">
{event.code}
</td>
<td>
{if event.user, do: event.user.email, else: gettext("No owner")}
</td>
<td>
{if event.started_at,
do: Calendar.strftime(event.started_at, "%Y-%m-%d %H:%M"),
else: gettext("Not started")}
</td>
<td>
{if event.expired_at,
do: Calendar.strftime(event.expired_at, "%Y-%m-%d %H:%M"),
else: gettext("Not expired")}
</td>
<td>
<div class="badge badge-outline">
{event.audience_peak || 0}
</div>
</td>
<td class="text-right">
<div class="flex justify-end gap-2">
<.link
navigate={~p"/admin/events/#{event}"}
class="btn btn-link btn-sm"
title={gettext("View event")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</.link>
<.link
navigate={~p"/admin/events/#{event}/edit"}
class="btn btn-link btn-sm text-primary"
title={gettext("Edit event")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
</.link>
<a
href="#"
phx-click="delete"
phx-value-id={event.id}
data-confirm={
gettext("Are you sure you want to delete this event?")
}
class="btn btn-link btn-sm text-error"
title={gettext("Delete event")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</a>
</div>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</:action>
</Flop.Phoenix.table>
<div class="flex justify-end mt-4">
<ClaperWeb.CoreComponents.pagination meta={@meta} path={~p"/admin/events"} />
</div>
</div>
<% :show -> %>

View File

@@ -4,7 +4,6 @@ defmodule ClaperWeb.AdminLive.UserLive do
alias Claper.Admin
alias Claper.Accounts
alias Claper.Accounts.User
alias ClaperWeb.Helpers.CSVExporter
@impl true
def mount(_params, session, socket) do
@@ -12,12 +11,7 @@ defmodule ClaperWeb.AdminLive.UserLive do
Gettext.put_locale(ClaperWeb.Gettext, locale)
end
{:ok,
socket
|> assign(:page_title, gettext("Users"))
|> assign(:users, list_users())
|> assign(:search, "")
|> assign(:current_sort, %{field: :na, order: :asc})}
{:ok, socket}
end
@impl true
@@ -25,10 +19,25 @@ defmodule ClaperWeb.AdminLive.UserLive do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
defp apply_action(socket, :index, params) do
{users, meta} = Admin.list_users_paginated(params)
socket
|> assign(:page_title, "Users")
|> assign(:page_title, gettext("Users"))
|> assign(:user, nil)
|> assign(:users, users)
|> assign(:meta, meta)
|> assign(:form, Phoenix.Component.to_form(meta))
|> assign_new(:fields, fn ->
[
email: [
label: nil,
placeholder: gettext("Search users..."),
type: "text",
op: :ilike_or
]
]
end)
end
defp apply_action(socket, :new, _params) do
@@ -57,142 +66,14 @@ defmodule ClaperWeb.AdminLive.UserLive do
{:noreply,
socket
|> put_flash(:info, gettext("User deleted successfully"))
|> assign(:users, list_users())}
|> push_patch(to: Flop.Phoenix.build_path(~p"/admin/users", socket.assigns.meta.flop))}
end
@impl true
def handle_event("search", %{"search" => search}, socket) do
users = search_users(search)
{:noreply, socket |> assign(:search, search) |> assign(:users, users)}
end
def handle_event("filter-users", unsigned_params, socket) do
flop = Flop.validate!(unsigned_params, for: User, replace_invalid_params: true)
to = Flop.Phoenix.build_path(~p"/admin/users", flop)
@impl true
def handle_event("sort", %{"field" => field}, socket) do
field_atom = String.to_existing_atom(field)
current_sort = socket.assigns.current_sort
new_direction =
if current_sort.field == field_atom do
if current_sort.order == :asc, do: :desc, else: :asc
else
:asc
end
users = sort_users(socket.assigns.users, field_atom, new_direction)
current_sort = %{field: field_atom, order: new_direction}
{:noreply,
socket
|> assign(:users, users)
|> assign(:current_sort, current_sort)}
end
@impl true
def handle_info({:search_filter_changed, %{search: search, filters: _filters}}, socket) do
users = search_users(search)
{:noreply, socket |> assign(:search, search) |> assign(:users, users)}
end
@impl true
def handle_info({:export_csv_requested, _params}, socket) do
filename = CSVExporter.generate_filename("users")
csv_content = CSVExporter.export_users_to_csv(socket.assigns.users)
{:noreply,
socket
|> put_flash(:info, gettext("Users exported successfully"))
|> push_event("download_csv", %{filename: filename, content: csv_content})}
end
@impl true
def handle_info({:table_sort_changed, sort_config}, socket) do
%{field: field, direction: direction} = sort_config
users = sort_users(socket.assigns.users, field, direction)
current_sort = %{field: field, order: direction}
{:noreply,
socket
|> assign(:users, users)
|> assign(:current_sort, current_sort)}
end
@impl true
def handle_info({:table_action, action, user, _user_id}, socket) do
case action do
:view ->
{:noreply, push_navigate(socket, to: ~p"/admin/users/#{user}")}
:edit ->
{:noreply, push_navigate(socket, to: ~p"/admin/users/#{user}/edit")}
:delete ->
{:ok, _} = Accounts.delete_user(user)
{:noreply,
socket
|> put_flash(:info, gettext("User deleted successfully"))
|> assign(:users, list_users())}
end
end
def list_users do
Admin.list_all_users()
end
defp search_users(search) when search == "", do: list_users()
defp search_users(search) do
Admin.list_all_users(%{"search" => search})
end
defp sort_users(users, field, order) do
Enum.sort_by(users, &Map.get(&1, field), order)
end
def sort_indicator(assigns) do
~H"""
<%= if @current_sort.field == @field do %>
<%= if @current_sort.order == :asc do %>
<svg
class="ml-2 h-5 w-5 text-gray-500 group-hover:text-gray-700"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 5.414V17a1 1 0 11-2 0V5.414L6.707 7.707a1 1 0 01-1.414 0z"
clip-rule="evenodd"
/>
</svg>
<% else %>
<svg
class="ml-2 h-5 w-5 text-gray-500 group-hover:text-gray-700"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M14.707 12.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 14.586V3a1 1 0 012 0v11.586l2.293-2.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
<% end %>
<% else %>
<svg
class="ml-2 h-5 w-5 text-gray-400 opacity-0 group-hover:opacity-100"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
<% end %>
"""
{:noreply, push_patch(socket, to: to)}
end
end

View File

@@ -1,230 +1,89 @@
<%= case @live_action do %>
<% :index -> %>
<div>
<div>
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold">{gettext("Users")}</h1>
<div class="flex space-x-3">
<%!-- <.link navigate={~p"/admin/users/new"} class="btn btn-primary btn-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
class="w-3 h-3"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="3"
d="M12 4v16m8-8H4"
/>
</svg>
{gettext("New user")}
</.link> --%>
</div>
</div>
</div>
<h1 class="text-3xl font-bold mb-6">{gettext("Users")}</h1>
<div class="mt-4">
<div class="py-4">
<!-- Search Bar -->
<div class="mb-6">
<form phx-change="search" class="flex w-full md:w-1/2 my-3">
<label class="input">
<svg
class="h-[1em] opacity-50"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2.5"
fill="none"
stroke="currentColor"
>
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input
type="search"
name="search"
value={@search}
placeholder={gettext("Search users...")}
/>
</label>
</form>
<.form
for={@form}
class="flex flex-col sm:flex-row gap-3 sm:[&>div]:basis-[250px]"
phx-change="filter-users"
>
<Flop.Phoenix.filter_fields :let={i} form={@form} fields={@fields}>
<ClaperWeb.CoreComponents.input
field={i.field}
label={i.label}
type={i.type}
phx-debounce="300"
{i.rest}
/>
</Flop.Phoenix.filter_fields>
</.form>
<Flop.Phoenix.table
items={@users}
meta={@meta}
path={~p"/admin/users"}
opts={[
container: true,
container_attrs: [class: "card bg-base-100 shadow-xl overflow-x-auto my-5"],
no_results_content: ~H"""
<p class="text-center py-10">{gettext("No users found")}</p>
""",
table_attrs: [class: "table table-zebra w-full"],
symbol_asc: ~H"""
<span aria-hidden="true"><ClaperWeb.Icons.arrow_up /></span>
""",
symbol_desc: ~H"""
<span aria-hidden="true"><ClaperWeb.Icons.arrow_down /></span>
"""
]}
>
<:col :let={user} label={gettext("Email")} field={:email}>{user.email}</:col>
<:col :let={user} label={gettext("Role")} field={:role_id}>
{if user.role, do: user.role.name, else: gettext("No role")}
</:col>
<:col :let={user} label={gettext("Status")} field={:confirmed_at}>
<%= if user.confirmed_at do %>
<span class="badge badge-success">{gettext("Confirmed")}</span>
<% else %>
<span class="badge badge-warning">{gettext("Unconfirmed")}</span>
<% end %>
</:col>
<:col :let={user} label={gettext("Created")} field={:inserted_at}>
{Calendar.strftime(user.inserted_at, "%Y-%m-%d %H:%M")}
</:col>
<:action :let={user} tbody_td_attrs={[class: "text-right"]}>
<div class="flex justify-end gap-2">
<.link
navigate={~p"/admin/users/#{user}"}
class="btn btn-link btn-sm"
title={gettext("View user")}
>
<ClaperWeb.Icons.eye />
</.link>
<.link
navigate={~p"/admin/users/#{user}/edit"}
class="btn btn-link btn-sm text-primary"
title={gettext("Edit user")}
>
<span class="hero-pencil-square w-5 h-5" />
</.link>
<a
href="#"
phx-click="delete"
phx-value-id={user.id}
data-confirm={gettext("Are you sure you want to delete this user?")}
class="btn btn-link btn-sm text-error"
title={gettext("Delete user")}
>
<span class="hero-trash w-5 h-5" />
</a>
</div>
<!-- Users Table -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-0">
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="email"
class="btn btn-ghost btn-sm"
>
{gettext("Email")} {sort_indicator(assigns |> Map.put(:field, :email))}
</button>
</th>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="role_id"
class="btn btn-ghost btn-sm"
>
{gettext("Role")} {sort_indicator(assigns |> Map.put(:field, :role_id))}
</button>
</th>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="confirmed_at"
class="btn btn-ghost btn-sm"
>
{gettext("Status")} {sort_indicator(
assigns
|> Map.put(:field, :confirmed_at)
)}
</button>
</th>
<th class="text-left">
<button
phx-click="sort"
phx-value-field="inserted_at"
class="btn btn-ghost btn-sm"
>
{gettext("Created")} {sort_indicator(
assigns
|> Map.put(:field, :inserted_at)
)}
</button>
</th>
<th class="text-right">
<span class="sr-only">{gettext("Actions")}</span>
</th>
</tr>
</thead>
<tbody>
<%= if Enum.empty?(@users) do %>
<tr>
<td colspan="5" class="text-center">
{gettext("No users found")}
</td>
</tr>
<% else %>
<%= for user <- @users do %>
<tr id={"user-#{user.id}"}>
<td class="font-medium">
{user.email}
</td>
<td>
{if user.role, do: user.role.name, else: gettext("No role")}
</td>
<td>
<%= if user.confirmed_at do %>
<span class="badge badge-success">
{gettext("Confirmed")}
</span>
<% else %>
<span class="badge badge-warning">
{gettext("Unconfirmed")}
</span>
<% end %>
</td>
<td>
{Calendar.strftime(user.inserted_at, "%Y-%m-%d %H:%M")}
</td>
<td class="text-right">
<div class="flex justify-end gap-2">
<.link
navigate={~p"/admin/users/#{user}"}
class="btn btn-link btn-sm"
title={gettext("View user")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</.link>
<.link
navigate={~p"/admin/users/#{user}/edit"}
class="btn btn-link btn-sm text-primary"
title={gettext("Edit user")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
</.link>
<a
href="#"
phx-click="delete"
phx-value-id={user.id}
data-confirm={
gettext("Are you sure you want to delete this user?")
}
class="btn btn-link btn-sm text-error"
title={gettext("Delete user")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</a>
</div>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</:action>
</Flop.Phoenix.table>
<div class="flex justify-end mt-4">
<ClaperWeb.CoreComponents.pagination meta={@meta} path={~p"/admin/users"} />
</div>
</div>
<% :show -> %>

View File

@@ -22,10 +22,14 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
Presentations.get_first_slide_url(event.presentation_file)
end
defp dom_id(component_id, suffix) do
"#{component_id}-#{suffix}"
end
defp render_grid_card(assigns) do
~H"""
<div
id={"event-#{@event.uuid}"}
id={dom_id(@id, "card")}
class="group relative bg-white border border-gray-200 rounded-2xl overflow-hidden hover:shadow-lg transition-shadow duration-200 h-96"
x-data="{showActions: false, showJoinMenu: false}"
@mouseenter="showActions = true"
@@ -49,7 +53,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<span class="text-sm font-medium text-gray-600">{gettext("Processing...")}</span>
</div>
</div>
<!-- Status Badge -->
<div class="absolute top-4 left-4 z-10">
<%= if Event.started?(@event) && !Event.finished?(@event) do %>
@@ -69,7 +73,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
<% end %>
</div>
<!-- LTI Badge -->
<div :if={@event.lti_resource} class="absolute top-4 right-4 z-10">
<span class="badge badge-neutral gap-1">
@@ -88,7 +92,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
LTI
</span>
</div>
<!-- Sliding Bottom Panel -->
<div
class="absolute bottom-0 left-0 right-0 bg-white transition-transform duration-300 ease-out z-20"
@@ -105,12 +109,12 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
# {@event.code}
</p>
</div>
<!-- 3-dot Menu -->
<div :if={not @is_leader} class="relative shrink-0">
<button
phx-click-away={JS.hide(to: "#dropdown-menu-#{@event.uuid}")}
phx-click={JS.toggle(to: "#dropdown-menu-#{@event.uuid}")}
phx-click-away={JS.hide(to: "##{dom_id(@id, "dropdown-menu")}")}
phx-click={JS.toggle(to: "##{dom_id(@id, "dropdown-menu")}")}
phx-target={@myself}
class="p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded"
>
@@ -126,7 +130,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</svg>
</button>
<div
id={"dropdown-menu-#{@event.uuid}"}
id={dom_id(@id, "dropdown-menu")}
phx-hook="Dropdown"
class="hidden absolute right-0 bottom-full mb-2 w-52 dropdown-content menu bg-base-100 rounded-box shadow-lg z-30"
>
@@ -135,7 +139,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
</div>
</div>
<!-- Action Buttons (revealed on hover) -->
<div
:if={@event.presentation_file.status == "done" && !Event.finished?(@event)}
@@ -247,14 +251,14 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
{gettext("End Event")}
</.link>
</div>
<!-- Error Status -->
<div :if={@event.presentation_file.status == "fail"} class="px-2 pb-2">
<span class="text-sm text-supporting-red-500">
{gettext("Error when processing the file")}
</span>
</div>
<!-- Finished Event Actions -->
<div :if={Event.finished?(@event)} class="px-2 pb-2">
<a href={~p"/events/#{@event.uuid}/stats"} class="btn btn-primary w-full">
@@ -277,7 +281,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
defp render_list_card(assigns) do
~H"""
<div class="w-full" id={"event-#{@event.uuid}"} x-data="{showJoinMenu: false}">
<div class="w-full" id={dom_id(@id, "card")} x-data="{showJoinMenu: false}">
<div class="bg-white rounded-2xl border border-gray-200 hover:shadow-lg transition-shadow duration-200">
<div class="p-4 flex items-center gap-4">
<!-- Thumbnail -->
@@ -297,7 +301,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<img src="/images/logo.svg" class="h-6 animate-pulse" alt="Loading" />
</div>
</div>
<!-- Event Info -->
<div class="min-w-0">
<div class="flex items-center gap-2">
@@ -341,19 +345,19 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<span class="font-medium uppercase"># {@event.code}</span>
<span
:if={!Event.finished?(@event) && !Event.started?(@event)}
id={"event-date-#{@event.uuid}"}
id={dom_id(@id, "event-date")}
phx-update="ignore"
>
{gettext("Starting on")}
<span x-text={"moment.utc('#{@event.started_at}').local().format('lll')"}></span>
</span>
<span :if={Event.finished?(@event)} id={"event-date-#{@event.uuid}"} phx-update="ignore">
<span :if={Event.finished?(@event)} id={dom_id(@id, "event-date")} phx-update="ignore">
{gettext("Finished on")}
<span x-text={"moment.utc('#{@event.expired_at}').local().format('lll')"}></span>
</span>
</div>
</div>
<!-- Actions -->
<div class="flex items-center gap-2 ml-auto">
<%= if @event.presentation_file.status == "done" && !Event.finished?(@event) do %>
@@ -491,12 +495,12 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
{gettext("View report")}
</a>
<% end %>
<!-- 3-dot Menu -->
<div :if={not @is_leader} class="relative">
<button
phx-click-away={JS.hide(to: "#dropdown-menu-#{@event.uuid}")}
phx-click={JS.toggle(to: "#dropdown-menu-#{@event.uuid}")}
phx-click-away={JS.hide(to: "##{dom_id(@id, "dropdown-menu")}")}
phx-click={JS.toggle(to: "##{dom_id(@id, "dropdown-menu")}")}
phx-target={@myself}
class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded"
>
@@ -512,7 +516,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</svg>
</button>
<div
id={"dropdown-menu-#{@event.uuid}"}
id={dom_id(@id, "dropdown-menu")}
phx-hook="Dropdown"
class="hidden absolute right-0 top-10 w-52 dropdown-content menu bg-base-100 rounded-box shadow-lg z-30"
>
@@ -528,7 +532,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
defp render_mobile_card(assigns) do
~H"""
<div class="w-full" id={"event-#{@event.uuid}"} x-data="{showJoinMenu: false}">
<div class="w-full" id={dom_id(@id, "card")} x-data="{showJoinMenu: false}">
<div class="bg-white rounded-3xl border border-gray-200 p-2">
<div class="flex flex-col gap-2">
<!-- Top Row: Thumbnail + Info + Menu -->
@@ -550,7 +554,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<img src="/images/logo.svg" class="h-8 animate-pulse" alt="Loading" />
</div>
</div>
<!-- Event Info -->
<div class="flex-1 min-w-0 py-1">
<h3 class="font-semibold text-gray-800 text-lg leading-tight truncate">
@@ -577,12 +581,12 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<% end %>
</div>
</div>
<!-- 3-dot Menu -->
<div :if={not @is_leader} class="relative shrink-0">
<button
phx-click-away={JS.hide(to: "#dropdown-menu-#{@event.uuid}")}
phx-click={JS.toggle(to: "#dropdown-menu-#{@event.uuid}")}
phx-click-away={JS.hide(to: "##{dom_id(@id, "dropdown-menu")}")}
phx-click={JS.toggle(to: "##{dom_id(@id, "dropdown-menu")}")}
phx-target={@myself}
class="p-1.5 text-gray-400 hover:text-gray-600"
>
@@ -598,7 +602,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</svg>
</button>
<div
id={"dropdown-menu-#{@event.uuid}"}
id={dom_id(@id, "dropdown-menu")}
phx-hook="Dropdown"
class="hidden absolute right-0 top-8 w-52 dropdown-content menu bg-base-100 rounded-box shadow-lg z-30"
>
@@ -606,7 +610,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
</div>
</div>
<!-- Bottom Row: Action Buttons -->
<div class="flex gap-2">
<%= if @event.presentation_file.status == "done" && !Event.finished?(@event) do %>
@@ -756,8 +760,16 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<ul class="w-full">
<%= if !Event.finished?(@event) && not @is_leader do %>
<li>
<.link patch={~p"/events/#{@event.uuid}/edit"} class="flex items-center gap-3 w-full px-3 py-2 hover:bg-gray-50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<.link
patch={~p"/events/#{@event.uuid}/edit"}
class="flex items-center gap-3 w-full px-3 py-2 hover:bg-gray-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path d="m5.433 13.917 1.262-3.155A4 4 0 0 1 7.58 9.42l6.92-6.918a2.121 2.121 0 0 1 3 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 0 1-.65-.65Z" />
<path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0 0 10 3H4.75A2.75 2.75 0 0 0 2 5.75v9.5A2.75 2.75 0 0 0 4.75 18h9.5A2.75 2.75 0 0 0 17 15.25V10a.75.75 0 0 0-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5Z" />
</svg>
@@ -765,8 +777,17 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</.link>
</li>
<li>
<button phx-value-id={@event.uuid} phx-click="duplicate" class="flex items-center gap-3 w-full px-3 py-2 hover:bg-gray-50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<button
phx-value-id={@event.uuid}
phx-click="duplicate"
class="flex items-center gap-3 w-full px-3 py-2 hover:bg-gray-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path d="M7 3.5A1.5 1.5 0 0 1 8.5 2h3.879a1.5 1.5 0 0 1 1.06.44l3.122 3.12A1.5 1.5 0 0 1 17 6.622V12.5a1.5 1.5 0 0 1-1.5 1.5h-1v-3.379a3 3 0 0 0-.879-2.121L10.5 5.379A3 3 0 0 0 8.379 4.5H7v-1Z" />
<path d="M4.5 6A1.5 1.5 0 0 0 3 7.5v9A1.5 1.5 0 0 0 4.5 18h7a1.5 1.5 0 0 0 1.5-1.5v-5.879a1.5 1.5 0 0 0-.44-1.06L9.44 6.439A1.5 1.5 0 0 0 8.378 6H4.5Z" />
</svg>
@@ -777,8 +798,17 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<%= if Event.finished?(@event) && not @is_leader do %>
<li>
<button phx-value-id={@event.uuid} phx-click="duplicate" class="flex items-center gap-3 w-full px-3 py-2 hover:bg-gray-50">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5">
<button
phx-value-id={@event.uuid}
phx-click="duplicate"
class="flex items-center gap-3 w-full px-3 py-2 hover:bg-gray-50"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path d="M7 3.5A1.5 1.5 0 0 1 8.5 2h3.879a1.5 1.5 0 0 1 1.06.44l3.122 3.12A1.5 1.5 0 0 1 17 6.622V12.5a1.5 1.5 0 0 1-1.5 1.5h-1v-3.379a3 3 0 0 0-.879-2.121L10.5 5.379A3 3 0 0 0 8.379 4.5H7v-1Z" />
<path d="M4.5 6A1.5 1.5 0 0 0 3 7.5v9A1.5 1.5 0 0 0 4.5 18h7a1.5 1.5 0 0 0 1.5-1.5v-5.879a1.5 1.5 0 0 0-.44-1.06L9.44 6.439A1.5 1.5 0 0 0 8.378 6H4.5Z" />
</svg>
@@ -796,8 +826,17 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
}
class="flex items-center gap-3 w-full px-3 py-2 hover:bg-gray-50 text-error"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
{gettext("Delete")}
</.link>

View File

@@ -191,7 +191,7 @@ defmodule ClaperWeb.EventLive.Index do
defp load_events(socket) do
params = %{
"page" => socket.assigns.page,
"page_size" => 5,
"page_size" => 6,
"search" => socket.assigns.search_query
}

View File

@@ -358,11 +358,11 @@
/>
<path d="M5.082 14.254a8.287 8.287 0 0 0-1.308 5.135 9.687 9.687 0 0 1-1.764-.44l-.115-.04a.563.563 0 0 1-.373-.487l-.01-.121a3.75 3.75 0 0 1 3.57-4.047ZM20.226 19.389a8.287 8.287 0 0 0-1.308-5.135 3.75 3.75 0 0 1 3.57 4.047l-.01.121a.563.563 0 0 1-.373.486l-.115.04c-.567.2-1.156.349-1.764.441Z" />
</svg>
<span class="text-sm font-bold text-gray-700">{gettext("Join")}</span>
<span id="attendees-count" phx-update="ignore" phx-hook="UpdateAttendees" class="text-sm font-bold text-gray-700">
<span class="text-gray-700">{gettext("Join")}</span>
<span id="attendees-count" phx-update="ignore" phx-hook="UpdateAttendees" class="text-gray-700">
{@attendees_nb}
</span>
<span class="text-sm font-bold text-gray-700">{gettext("attendees")}</span>
<span class="text-gray-700">{gettext("attendees")}</span>
<!-- Arrow icon -->
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -66,18 +66,51 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
<div class="grid grid-cols-4 gap-1">
<.action_button patch={~p"/e/#{@event_code}/manage/add/poll"} label={gettext("Poll")}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</.action_button>
<.action_button patch={~p"/e/#{@event_code}/manage/add/form"} label={gettext("Form")}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg>
</.action_button>
<.action_button patch={~p"/e/#{@event_code}/manage/add/quiz"} label={gettext("Quiz")}>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
/>
</svg>
</.action_button>
@@ -86,8 +119,19 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
@click="open = !open"
class="flex flex-col items-center justify-center gap-0.5 py-2 rounded-xl text-gray-600 transition-colors hover:bg-primary-50 hover:text-primary-600 w-full"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-primary-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z"
/>
</svg>
<span class="font-bold text-xs leading-normal">{gettext("More")}</span>
</button>
@@ -108,8 +152,19 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
title={gettext("Poll")}
description={gettext("Add a poll to gauge your audience's opinion.")}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</.popup_item>
<.popup_item
@@ -117,8 +172,19 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
title={gettext("Form")}
description={gettext("Create a form to collect feedback from your audience.")}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
/>
</svg>
</.popup_item>
<.popup_item
@@ -126,8 +192,19 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
title={gettext("Web Content")}
description={gettext("Embed external web content into your presentation.")}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.25 9.75L16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
</.popup_item>
<.popup_item
@@ -135,8 +212,19 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
title={gettext("Quiz")}
description={gettext("Test your audience's knowledge with a quiz.")}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z"
/>
</svg>
</.popup_item>
</div>
@@ -254,7 +342,13 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
patch={edit_path(@event_code, interaction)}
class="flex items-center justify-center rounded-full w-[42px] h-[41px] shrink-0 border border-[#140553] text-[#140553]"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M3.33301 17.4998H16.6663M4.72134 10.989C4.36602 11.3451 4.16643 11.8276 4.16634 12.3306V14.9998H6.85217C7.35551 14.9998 7.83801 14.7998 8.19384 14.4431L16.1105 6.52229C16.4657 6.16612 16.6652 5.68364 16.6652 5.18063C16.6652 4.67761 16.4657 4.19513 16.1105 3.83896L15.3288 3.05563C15.1526 2.87925 14.9432 2.73935 14.7129 2.64393C14.4825 2.54851 14.2355 2.49943 13.9862 2.49951C13.7368 2.49959 13.4899 2.54882 13.2596 2.64438C13.0292 2.73995 12.82 2.87998 12.6438 3.05646L4.72134 10.989Z"
stroke="#140553"
@@ -282,7 +376,14 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
disabled={@page == 0}
class="p-1 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
@@ -295,7 +396,14 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
disabled={@page >= @total_pages - 1}
class="p-1 rounded-lg text-gray-400 hover:text-gray-600 hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
@@ -352,7 +460,6 @@ defmodule ClaperWeb.EventLive.ManageInteractionListComponent do
~H"""
<.link
patch={@patch}
@click="open = false"
class="flex items-center gap-3 p-2 rounded-xl transition-colors hover:bg-primary-50"
>
<div class="flex items-center justify-center w-9 h-9 rounded-full bg-primary-50 text-primary-500 shrink-0">

View File

@@ -15,10 +15,10 @@ msgstr ""
msgid "Settings"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:74
#: lib/claper_web/live/admin_live/user_live.html.heex:254
#: lib/claper_web/live/admin_live/user_live.html.heex:72
#: lib/claper_web/live/admin_live/user_live.html.heex:291
#: lib/claper_web/live/admin_live/user_live/form_component.ex:18
#: lib/claper_web/live/event_live/manage.ex:842
#: lib/claper_web/live/event_live/manage.ex:918
#: lib/claper_web/live/form_live/form_component.html.heex:32
#: lib/claper_web/live/user_settings_live/show.html.heex:24
#: lib/claper_web/templates/user_registration/new.html.heex:29
@@ -47,7 +47,7 @@ msgid "Change"
msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:107
#: lib/claper_web/live/admin_live/event_live.html.heex:83
#: lib/claper_web/live/admin_live/event_live.html.heex:81
#: lib/claper_web/live/admin_live/event_live/form_component.ex:32
#: lib/claper_web/live/event_live/event_form_component.html.heex:276
#, elixir-autogen, elixir-format
@@ -189,10 +189,10 @@ msgstr ""
msgid "Created successfully"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:259
#: lib/claper_web/live/admin_live/event_live.html.heex:296
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:240
#: lib/claper_web/live/admin_live/user_live.html.heex:240
#: lib/claper_web/live/event_live/event_card_component.ex:772
#: lib/claper_web/live/admin_live/user_live.html.heex:277
#: lib/claper_web/live/event_live/event_card_component.ex:764
#: lib/claper_web/live/event_live/form_component.ex:97
#: lib/claper_web/live/event_live/index.ex:168
#, elixir-autogen, elixir-format
@@ -212,10 +212,10 @@ msgid "Create"
msgstr ""
#: lib/claper_web/live/embed_live/form_component.html.heex:64
#: lib/claper_web/live/event_live/event_card_component.ex:837
#: lib/claper_web/live/event_live/event_card_component.ex:802
#: lib/claper_web/live/event_live/event_form_component.html.heex:45
#: lib/claper_web/live/event_live/event_form_component.html.heex:401
#: lib/claper_web/live/event_live/manage_audience_responses_component.ex:252
#: lib/claper_web/live/event_live/manage_audience_responses_component.ex:251
#: lib/claper_web/live/event_live/manageable_post_component.ex:105
#: lib/claper_web/live/event_live/post_component.ex:70
#: lib/claper_web/live/event_live/post_component.ex:142
@@ -566,7 +566,7 @@ msgstr ""
msgid "No messages has been sent"
msgstr ""
#: lib/claper_web/live/event_live/event_card_component.ex:819
#: lib/claper_web/live/event_live/event_card_component.ex:793
#, elixir-autogen, elixir-format
msgid "This will delete all data related to your event, this cannot be undone. Confirm ?"
msgstr ""
@@ -691,13 +691,13 @@ msgid "Form"
msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:106
#: lib/claper_web/live/admin_live/event_live.html.heex:74
#: lib/claper_web/live/admin_live/event_live.html.heex:273
#: lib/claper_web/live/admin_live/event_live.html.heex:72
#: lib/claper_web/live/admin_live/event_live.html.heex:310
#: lib/claper_web/live/admin_live/event_live/form_component.ex:19
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:74
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:254
#: lib/claper_web/live/admin_live/oidc_provider_live/form_component.ex:24
#: lib/claper_web/live/event_live/manage.ex:841
#: lib/claper_web/live/event_live/manage.ex:917
#, elixir-autogen, elixir-format
msgid "Name"
msgstr ""
@@ -728,7 +728,7 @@ msgstr ""
msgid "Text"
msgstr ""
#: lib/claper_web/live/event_live/manage_audience_responses_component.ex:249
#: lib/claper_web/live/event_live/manage_audience_responses_component.ex:248
#, elixir-autogen, elixir-format
msgid "This cannot be undone, confirm ?"
msgstr ""
@@ -1019,6 +1019,9 @@ msgstr ""
msgid "Finish"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:40
#: lib/claper_web/live/admin_live/event_live.html.heex:276
#: lib/claper_web/live/admin_live/user_live.html.heex:257
#: lib/claper_web/live/event_live/manage_interaction_options_component.ex:188
#: lib/claper_web/live/event_live/quiz_component.ex:161
#: lib/claper_web/live/event_live/quiz_component.ex:209
@@ -1112,7 +1115,7 @@ msgid "Questions will appear here."
msgstr ""
#: lib/claper_web/templates/layout/_user_menu.html.heex:13
#: lib/claper_web/templates/layout/admin.html.heex:262
#: lib/claper_web/templates/layout/admin.html.heex:278
#, elixir-autogen, elixir-format
msgid "Documentation"
msgstr ""
@@ -1286,8 +1289,8 @@ msgstr ""
msgid "Options"
msgstr ""
#: lib/claper_web/live/event_live/event_card_component.ex:790
#: lib/claper_web/live/event_live/event_card_component.ex:811
#: lib/claper_web/live/event_live/event_card_component.ex:773
#: lib/claper_web/live/event_live/event_card_component.ex:785
#, elixir-autogen, elixir-format
msgid "Duplicate"
msgstr ""
@@ -1337,12 +1340,12 @@ msgstr ""
msgid "Try logging in again"
msgstr ""
#: lib/claper_web/live/event_live/manage.ex:820
#: lib/claper_web/live/event_live/manage.ex:890
#, elixir-autogen, elixir-format
msgid "No"
msgstr ""
#: lib/claper_web/live/event_live/manage.ex:820
#: lib/claper_web/live/event_live/manage.ex:890
#, elixir-autogen, elixir-format
msgid "Yes"
msgstr ""
@@ -1464,6 +1467,9 @@ msgstr ""
msgid "New quiz"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:34
#: lib/claper_web/live/admin_live/event_live.html.heex:261
#: lib/claper_web/live/admin_live/user_live.html.heex:242
#: lib/claper_web/live/event_live/manage_interaction_options_component.ex:180
#, elixir-autogen, elixir-format
msgid "Previous"
@@ -1686,9 +1692,10 @@ msgstr ""
msgid "Account is confirmed and active"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:126
#: lib/claper_web/components/core_components.ex:368
#: lib/claper_web/live/admin_live/event_live.html.heex:124
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:108
#: lib/claper_web/live/admin_live/user_live.html.heex:111
#: lib/claper_web/live/admin_live/user_live.html.heex:109
#, elixir-autogen, elixir-format
msgid "Actions"
msgstr ""
@@ -1698,7 +1705,7 @@ msgstr ""
msgid "Active Events"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:216
#: lib/claper_web/live/admin_live/event_live.html.heex:214
#, elixir-autogen, elixir-format
msgid "Are you sure you want to delete this event?"
msgstr ""
@@ -1708,7 +1715,7 @@ msgstr ""
msgid "Are you sure you want to delete this provider?"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:197
#: lib/claper_web/live/admin_live/user_live.html.heex:195
#, elixir-autogen, elixir-format
msgid "Are you sure you want to delete this user?"
msgstr ""
@@ -1719,8 +1726,8 @@ msgid "Assigned User"
msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:110
#: lib/claper_web/live/admin_live/event_live.html.heex:119
#: lib/claper_web/live/admin_live/event_live.html.heex:299
#: lib/claper_web/live/admin_live/event_live.html.heex:117
#: lib/claper_web/live/admin_live/event_live.html.heex:336
#, elixir-autogen, elixir-format
msgid "Audience Peak"
msgstr ""
@@ -1758,13 +1765,13 @@ msgstr ""
msgid "Completed"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:134
#: lib/claper_web/live/admin_live/user_live.html.heex:268
#: lib/claper_web/live/admin_live/user_live.html.heex:132
#: lib/claper_web/live/admin_live/user_live.html.heex:305
#, elixir-autogen, elixir-format
msgid "Confirmed"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:291
#: lib/claper_web/live/admin_live/user_live.html.heex:328
#, elixir-autogen, elixir-format
msgid "Confirmed At"
msgstr ""
@@ -1785,14 +1792,14 @@ msgid "Create User"
msgstr ""
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:101
#: lib/claper_web/live/admin_live/user_live.html.heex:104
#: lib/claper_web/live/admin_live/user_live.html.heex:102
#, elixir-autogen, elixir-format
msgid "Created"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:305
#: lib/claper_web/live/admin_live/event_live.html.heex:342
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:296
#: lib/claper_web/live/admin_live/user_live.html.heex:278
#: lib/claper_web/live/admin_live/user_live.html.heex:315
#, elixir-autogen, elixir-format
msgid "Created At"
msgstr ""
@@ -1802,7 +1809,7 @@ msgstr ""
msgid "Currently running"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:219
#: lib/claper_web/live/admin_live/event_live.html.heex:217
#, elixir-autogen, elixir-format
msgid "Delete event"
msgstr ""
@@ -1812,7 +1819,7 @@ msgstr ""
msgid "Delete provider"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:200
#: lib/claper_web/live/admin_live/user_live.html.heex:198
#, elixir-autogen, elixir-format
msgid "Delete user"
msgstr ""
@@ -1823,10 +1830,10 @@ msgid "Edit OIDC Provider"
msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:176
#: lib/claper_web/live/admin_live/event_live.ex:41
#: lib/claper_web/live/admin_live/event_live.html.heex:194
#: lib/claper_web/live/admin_live/event_live.html.heex:348
#: lib/claper_web/live/admin_live/event_live.html.heex:358
#: lib/claper_web/live/admin_live/event_live.ex:62
#: lib/claper_web/live/admin_live/event_live.html.heex:192
#: lib/claper_web/live/admin_live/event_live.html.heex:385
#: lib/claper_web/live/admin_live/event_live.html.heex:395
#, elixir-autogen, elixir-format
msgid "Edit event"
msgstr ""
@@ -1838,10 +1845,10 @@ msgstr ""
msgid "Edit provider"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.ex:42
#: lib/claper_web/live/admin_live/user_live.html.heex:175
#: lib/claper_web/live/admin_live/user_live.html.heex:329
#: lib/claper_web/live/admin_live/user_live.html.heex:339
#: lib/claper_web/live/admin_live/user_live.ex:63
#: lib/claper_web/live/admin_live/user_live.html.heex:173
#: lib/claper_web/live/admin_live/user_live.html.heex:366
#: lib/claper_web/live/admin_live/user_live.html.heex:376
#, elixir-autogen, elixir-format
msgid "Edit user"
msgstr ""
@@ -1896,14 +1903,14 @@ msgstr ""
msgid "Event created successfully"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.ex:58
#: lib/claper_web/live/admin_live/event_live.ex:110
#: lib/claper_web/live/admin_live/event_live.ex:79
#: lib/claper_web/live/admin_live/event_live.ex:142
#, elixir-autogen, elixir-format
msgid "Event deleted successfully"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.ex:47
#: lib/claper_web/live/admin_live/event_live.html.heex:253
#: lib/claper_web/live/admin_live/event_live.ex:68
#: lib/claper_web/live/admin_live/event_live.html.heex:290
#, elixir-autogen, elixir-format
msgid "Event details"
msgstr ""
@@ -1913,20 +1920,21 @@ msgstr ""
msgid "Event updated successfully"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.ex:16
#: lib/claper_web/live/admin_live/event_live.ex:22
#: lib/claper_web/live/admin_live/event_live.ex:50
#: lib/claper_web/live/admin_live/event_live.html.heex:6
#: lib/claper_web/templates/layout/admin.html.heex:214
#, elixir-autogen, elixir-format
msgid "Events"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.ex:92
#: lib/claper_web/live/admin_live/event_live.ex:124
#, elixir-autogen, elixir-format
msgid "Events exported successfully"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:107
#: lib/claper_web/live/admin_live/event_live.html.heex:291
#: lib/claper_web/live/admin_live/event_live.html.heex:105
#: lib/claper_web/live/admin_live/event_live.html.heex:328
#: lib/claper_web/live/admin_live/event_live/form_component.ex:57
#, elixir-autogen, elixir-format
msgid "Expired At"
@@ -1990,22 +1998,22 @@ msgstr ""
msgid "Last 30 days"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:311
#: lib/claper_web/live/admin_live/event_live.html.heex:348
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:302
#: lib/claper_web/live/admin_live/user_live.html.heex:284
#: lib/claper_web/live/admin_live/user_live.html.heex:321
#, elixir-autogen, elixir-format
msgid "Last Updated"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.ex:35
#: lib/claper_web/live/admin_live/event_live.html.heex:325
#: lib/claper_web/live/admin_live/event_live.html.heex:335
#: lib/claper_web/live/admin_live/event_live.ex:56
#: lib/claper_web/live/admin_live/event_live.html.heex:362
#: lib/claper_web/live/admin_live/event_live.html.heex:372
#, elixir-autogen, elixir-format
msgid "New event"
msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:201
#: lib/claper_web/live/admin_live/event_live.html.heex:134
#: lib/claper_web/live/admin_live/event_live.html.heex:132
#, elixir-autogen, elixir-format
msgid "No events found"
msgstr ""
@@ -2015,31 +2023,31 @@ msgstr ""
msgid "No file chosen"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:147
#: lib/claper_web/live/admin_live/event_live.html.heex:279
#: lib/claper_web/live/admin_live/event_live.html.heex:145
#: lib/claper_web/live/admin_live/event_live.html.heex:316
#, elixir-autogen, elixir-format
msgid "No owner"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:129
#: lib/claper_web/live/admin_live/user_live.html.heex:260
#: lib/claper_web/live/admin_live/user_live.html.heex:127
#: lib/claper_web/live/admin_live/user_live.html.heex:297
#, elixir-autogen, elixir-format
msgid "No role"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:119
#: lib/claper_web/live/admin_live/user_live.html.heex:117
#, elixir-autogen, elixir-format
msgid "No users found"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:157
#: lib/claper_web/live/admin_live/event_live.html.heex:295
#: lib/claper_web/live/admin_live/event_live.html.heex:155
#: lib/claper_web/live/admin_live/event_live.html.heex:332
#, elixir-autogen, elixir-format
msgid "Not expired"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:152
#: lib/claper_web/live/admin_live/event_live.html.heex:287
#: lib/claper_web/live/admin_live/event_live.html.heex:150
#: lib/claper_web/live/admin_live/event_live.html.heex:324
#, elixir-autogen, elixir-format
msgid "Not started"
msgstr ""
@@ -2071,8 +2079,8 @@ msgid "OIDC scopes to request (defaults to 'openid email profile')"
msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:108
#: lib/claper_web/live/admin_live/event_live.html.heex:87
#: lib/claper_web/live/admin_live/event_live.html.heex:277
#: lib/claper_web/live/admin_live/event_live.html.heex:85
#: lib/claper_web/live/admin_live/event_live.html.heex:314
#, elixir-autogen, elixir-format
msgid "Owner"
msgstr ""
@@ -2113,8 +2121,8 @@ msgstr ""
msgid "Response Type"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:83
#: lib/claper_web/live/admin_live/user_live.html.heex:258
#: lib/claper_web/live/admin_live/user_live.html.heex:81
#: lib/claper_web/live/admin_live/user_live.html.heex:295
#: lib/claper_web/live/admin_live/user_live/form_component.ex:46
#, elixir-autogen, elixir-format
msgid "Role"
@@ -2138,7 +2146,7 @@ msgstr ""
msgid "Scope"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:55
#: lib/claper_web/live/admin_live/event_live.html.heex:54
#, elixir-autogen, elixir-format
msgid "Search events..."
msgstr ""
@@ -2153,7 +2161,7 @@ msgstr ""
msgid "Search providers..."
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:55
#: lib/claper_web/live/admin_live/user_live.html.heex:54
#, elixir-autogen, elixir-format
msgid "Search users..."
msgstr ""
@@ -2173,8 +2181,8 @@ msgstr ""
msgid "Start Date"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:95
#: lib/claper_web/live/admin_live/event_live.html.heex:283
#: lib/claper_web/live/admin_live/event_live.html.heex:93
#: lib/claper_web/live/admin_live/event_live.html.heex:320
#: lib/claper_web/live/admin_live/event_live/form_component.ex:46
#, elixir-autogen, elixir-format
msgid "Started At"
@@ -2183,8 +2191,8 @@ msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:111
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:92
#: lib/claper_web/live/admin_live/oidc_provider_live.html.heex:282
#: lib/claper_web/live/admin_live/user_live.html.heex:92
#: lib/claper_web/live/admin_live/user_live.html.heex:264
#: lib/claper_web/live/admin_live/user_live.html.heex:90
#: lib/claper_web/live/admin_live/user_live.html.heex:301
#, elixir-autogen, elixir-format
msgid "Status"
msgstr ""
@@ -2214,8 +2222,8 @@ msgstr ""
msgid "Total Users"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:138
#: lib/claper_web/live/admin_live/user_live.html.heex:272
#: lib/claper_web/live/admin_live/user_live.html.heex:136
#: lib/claper_web/live/admin_live/user_live.html.heex:309
#, elixir-autogen, elixir-format
msgid "Unconfirmed"
msgstr ""
@@ -2235,7 +2243,7 @@ msgstr ""
msgid "Update User"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:250
#: lib/claper_web/live/admin_live/user_live.html.heex:287
#, elixir-autogen, elixir-format
msgid "User Account"
msgstr ""
@@ -2250,14 +2258,14 @@ msgstr ""
msgid "User created successfully"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.ex:59
#: lib/claper_web/live/admin_live/user_live.ex:127
#: lib/claper_web/live/admin_live/user_live.ex:80
#: lib/claper_web/live/admin_live/user_live.ex:164
#, elixir-autogen, elixir-format
msgid "User deleted successfully"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.ex:48
#: lib/claper_web/live/admin_live/user_live.html.heex:234
#: lib/claper_web/live/admin_live/user_live.ex:69
#: lib/claper_web/live/admin_live/user_live.html.heex:271
#, elixir-autogen, elixir-format
msgid "User details"
msgstr ""
@@ -2277,20 +2285,21 @@ msgstr ""
msgid "User's email address (must be unique)"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.ex:17
#: lib/claper_web/live/admin_live/user_live.ex:23
#: lib/claper_web/live/admin_live/user_live.ex:51
#: lib/claper_web/live/admin_live/user_live.html.heex:6
#: lib/claper_web/templates/layout/admin.html.heex:230
#, elixir-autogen, elixir-format
msgid "Users"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.ex:97
#: lib/claper_web/live/admin_live/user_live.ex:135
#, elixir-autogen, elixir-format
msgid "Users exported successfully"
msgstr ""
#: lib/claper_web/live/admin_live/dashboard_live.html.heex:151
#: lib/claper_web/live/admin_live/event_live.html.heex:169
#: lib/claper_web/live/admin_live/event_live.html.heex:167
#, elixir-autogen, elixir-format
msgid "View event"
msgstr ""
@@ -2300,7 +2309,7 @@ msgstr ""
msgid "View provider"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:150
#: lib/claper_web/live/admin_live/user_live.html.heex:148
#, elixir-autogen, elixir-format
msgid "View user"
msgstr ""
@@ -2315,7 +2324,7 @@ msgstr ""
msgid "Whether this provider is currently active and available for authentication"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:301
#: lib/claper_web/live/admin_live/event_live.html.heex:338
#: lib/claper_web/live/event_live/manage.html.heex:365
#, elixir-autogen, elixir-format
msgid "attendees"
@@ -2342,7 +2351,7 @@ msgstr ""
msgid "vs last month"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:256
#: lib/claper_web/live/admin_live/event_live.html.heex:293
#, elixir-autogen, elixir-format
msgid "Back to events"
msgstr ""
@@ -2352,7 +2361,7 @@ msgstr ""
msgid "Back to providers"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.html.heex:237
#: lib/claper_web/live/admin_live/user_live.html.heex:274
#, elixir-autogen, elixir-format
msgid "Back to users"
msgstr ""
@@ -2365,9 +2374,9 @@ msgstr ""
msgid "New provider"
msgstr ""
#: lib/claper_web/live/admin_live/user_live.ex:36
#: lib/claper_web/live/admin_live/user_live.html.heex:306
#: lib/claper_web/live/admin_live/user_live.html.heex:316
#: lib/claper_web/live/admin_live/user_live.ex:57
#: lib/claper_web/live/admin_live/user_live.html.heex:343
#: lib/claper_web/live/admin_live/user_live.html.heex:353
#, elixir-autogen, elixir-format
msgid "New user"
msgstr ""
@@ -2394,7 +2403,7 @@ msgstr ""
msgid "Admin"
msgstr ""
#: lib/claper_web/templates/layout/admin.html.heex:285
#: lib/claper_web/templates/layout/admin.html.heex:301
#, elixir-autogen, elixir-format
msgid "Back to app"
msgstr ""
@@ -2651,3 +2660,116 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "New presentation"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:67
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:105
#, elixir-autogen, elixir-format
msgid "Action"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.ex:39
#, elixir-autogen, elixir-format
msgid "All actions"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.ex:50
#, elixir-autogen, elixir-format
msgid "Audit Log Details"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.ex:24
#: lib/claper_web/templates/layout/admin.html.heex:246
#, elixir-autogen, elixir-format
msgid "Audit Logs"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:66
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:99
#, elixir-autogen, elixir-format
msgid "ID"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:78
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:152
#, elixir-autogen, elixir-format
msgid "Metadata"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:55
#, elixir-autogen, elixir-format
msgid "No results."
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:145
#, elixir-autogen, elixir-format
msgid "Resource ID"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:137
#, elixir-autogen, elixir-format
msgid "Resource Type"
msgstr ""
#: lib/claper_web/live/event_live/manage.ex:898
#: lib/claper_web/live/event_live/manage.ex:941
#: lib/claper_web/live/event_live/manage.ex:957
#: lib/claper_web/live/event_live/manage.ex:999
#, elixir-autogen, elixir-format
msgid "Resource not found"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.ex:33
#, elixir-autogen, elixir-format
msgid "Search by user email"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:247
#: lib/claper_web/live/admin_live/user_live.html.heex:228
#, elixir-autogen, elixir-format
msgid "Showing"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:73
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:130
#, elixir-autogen, elixir-format
msgid "Timestamp"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:70
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:111
#, elixir-autogen, elixir-format
msgid "User"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:88
#, elixir-autogen, elixir-format
msgid "View details for log #%{id}"
msgstr ""
#: lib/claper_web/live/admin_live/audit_log_live.html.heex:119
#, elixir-autogen, elixir-format
msgid "View profile for %{email}"
msgstr ""
#: lib/claper_web/components/core_components.ex:74
#, elixir-autogen, elixir-format
msgid "close"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:247
#: lib/claper_web/live/admin_live/user_live.html.heex:228
#, elixir-autogen, elixir-format
msgid "of"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:247
#: lib/claper_web/live/admin_live/user_live.html.heex:228
#, elixir-autogen, elixir-format
msgid "results"
msgstr ""
#: lib/claper_web/live/admin_live/event_live.html.heex:247
#: lib/claper_web/live/admin_live/user_live.html.heex:228
#, elixir-autogen, elixir-format
msgid "to"
msgstr ""

View File

@@ -0,0 +1,93 @@
defmodule Claper.AdminTest do
use Claper.DataCase
alias Claper.Admin
import Claper.AccountsFixtures
import Claper.EventsFixtures
describe "list_users_paginated/1" do
test "returns paginated users" do
search = "admin-pagination-user"
users =
for index <- 1..21 do
user_fixture(%{email: "#{search}-#{index}@example.com"})
end
{page_1_users, meta_1} =
Admin.list_users_paginated(%{
"page" => 1,
"page_size" => 20,
"order_by" => ["email"],
"order_directions" => ["asc"],
"filters" => [%{"field" => "email", "op" => "ilike_or", "value" => search}]
})
{page_2_users, meta_2} =
Admin.list_users_paginated(%{
"page" => 2,
"page_size" => 20,
"order_by" => ["email"],
"order_directions" => ["asc"],
"filters" => [%{"field" => "email", "op" => "ilike_or", "value" => search}]
})
assert meta_1.total_count == 21
assert meta_1.total_pages == 2
assert meta_2.total_count == 21
assert meta_2.total_pages == 2
assert length(page_1_users) == 20
assert length(page_2_users) == 1
expected_user_ids =
users
|> Enum.sort_by(& &1.email, :asc)
|> Enum.map(& &1.id)
assert Enum.map(page_1_users ++ page_2_users, & &1.id) == expected_user_ids
end
end
describe "list_events_paginated/1" do
test "returns paginated events" do
search = "admin-pagination-event"
events =
for index <- 1..21 do
event_fixture(%{
name: "#{search} #{index}",
started_at: NaiveDateTime.add(~N[2026-01-01 00:00:00], index, :second)
})
end
{page_1_events, meta_1} =
Admin.list_events_paginated(%{
"page" => 1,
"page_size" => 20,
"filters" => [%{"field" => "name", "op" => "ilike_or", "value" => search}]
})
{page_2_events, meta_2} =
Admin.list_events_paginated(%{
"page" => 2,
"page_size" => 20,
"filters" => [%{"field" => "name", "op" => "ilike_or", "value" => search}]
})
assert meta_1.total_count == 21
assert meta_1.total_pages == 2
assert meta_2.total_count == 21
assert meta_2.total_pages == 2
assert length(page_1_events) == 20
assert length(page_2_events) == 1
expected_event_ids =
events
|> Enum.sort_by(& &1.started_at, {:desc, NaiveDateTime})
|> Enum.map(& &1.id)
assert Enum.map(page_1_events ++ page_2_events, & &1.id) == expected_event_ids
end
end
end