Refactor event card component to support mobile view and improve rendering logic

This commit is contained in:
Alex Lion
2026-01-24 17:15:57 +01:00
parent 46971719d6
commit eb5f3fb18f
3 changed files with 368 additions and 42 deletions

View File

@@ -11,10 +11,10 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
|> assign_new(:view_mode, fn -> "grid" end)
|> assign(:thumbnail_url, get_thumbnail_url(assigns.event))
if assigns.view_mode == "grid" do
render_grid_card(assigns)
else
render_list_card(assigns)
case assigns.view_mode do
"grid" -> render_grid_card(assigns)
"mobile" -> render_mobile_card(assigns)
_ -> render_list_card(assigns)
end
end
@@ -41,7 +41,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
<% end %>
</div>
<!-- Status Badge -->
<div class="absolute top-4 left-4 z-10">
<%= if Event.started?(@event) && !Event.finished?(@event) do %>
@@ -61,7 +61,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
<% end %>
</div>
<!-- LTI Badge -->
<div :if={@event.lti_resource} class="absolute top-4 right-4 z-10">
<div class="px-2 py-0.5 text-xs font-medium rounded-md bg-gray-500 text-white flex items-center gap-1">
@@ -80,7 +80,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<span>LTI</span>
</div>
</div>
<!-- Sliding Bottom Panel -->
<div
class="absolute bottom-0 left-0 right-0 bg-white transition-transform duration-300 ease-out z-20"
@@ -97,8 +97,8 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
# {@event.code}
</p>
</div>
<!-- 3-dot Menu -->
<!-- 3-dot Menu -->
<div class="relative shrink-0">
<button
phx-click-away={JS.hide(to: "#dropdown-menu-#{@event.uuid}")}
@@ -127,8 +127,8 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
</div>
</div>
<!-- Action Buttons (revealed on hover) -->
<!-- Action Buttons (revealed on hover) -->
<div
:if={@event.presentation_file.status == "done" && !Event.finished?(@event)}
class="px-2 pb-2 flex gap-2"
@@ -233,15 +233,13 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
viewBox="0 0 20 20"
fill="currentColor"
>
<path
d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z"
/>
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
</svg>
{gettext("End Event")}
</.link>
</div>
<!-- Processing Status -->
<!-- Processing Status -->
<div
:if={@event.presentation_file.status == "progress"}
class="px-2 pb-2 flex items-center gap-2"
@@ -249,15 +247,15 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
<img src="/images/loading.gif" class="h-6" />
<span class="text-sm text-gray-500">{gettext("Processing your file...")}</span>
</div>
<!-- Error Status -->
<!-- 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 -->
<!-- Finished Event Actions -->
<div :if={Event.finished?(@event)} class="px-2 pb-2">
<a
href={~p"/events/#{@event.uuid}/stats"}
@@ -295,7 +293,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
<% end %>
</div>
<!-- Event Info -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
@@ -340,7 +338,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</span>
</div>
</div>
<!-- Status Badge -->
<div class="shrink-0">
<%= if Event.started?(@event) && !Event.finished?(@event) do %>
@@ -360,7 +358,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
</div>
<% end %>
</div>
<!-- Actions -->
<div class="shrink-0 flex items-center gap-2">
<%= if @event.presentation_file.status == "done" && !Event.finished?(@event) do %>
@@ -404,7 +402,7 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
{gettext("View report")}
</a>
<% end %>
<!-- 3-dot Menu -->
<div class="relative">
<button
@@ -439,6 +437,172 @@ defmodule ClaperWeb.EventLive.EventCardComponent do
"""
end
defp render_mobile_card(assigns) do
~H"""
<li class="w-full" id={"event-#{@event.uuid}"}>
<div class="bg-white rounded-3xl border border-gray-200 overflow-hidden p-2">
<div class="flex flex-col gap-2">
<!-- Top Row: Thumbnail + Info + Menu -->
<div class="flex gap-3 items-start">
<!-- Thumbnail -->
<div class="shrink-0 w-28 h-24 rounded-2xl overflow-hidden bg-gray-100">
<%= if @thumbnail_url do %>
<img src={@thumbnail_url} alt={@event.name} class="w-full h-full object-cover" />
<% else %>
<div class="w-full h-full flex items-center justify-center">
<img src="/images/logo.svg" class="h-8 opacity-30" alt="Claper" />
</div>
<% end %>
</div>
<!-- Event Info -->
<div class="flex-1 min-w-0 py-1">
<h3 class="font-semibold text-gray-800 text-lg leading-tight truncate">
{@event.name}
</h3>
<div class="flex items-center gap-2 mt-1">
<span class="text-xs text-gray-500"># {@event.code}</span>
<!-- Status Badge -->
<%= if Event.started?(@event) && !Event.finished?(@event) do %>
<span class="px-2 py-0.5 text-[10px] font-medium rounded-tl-none rounded-lg bg-primary-500 text-white">
{gettext("En direct")}
</span>
<% end %>
<%= if !Event.started?(@event) && !Event.finished?(@event) do %>
<span class="px-2 py-0.5 text-[10px] font-medium rounded-tl-none rounded-lg bg-green-100 text-green-800">
{gettext("Incoming")}
</span>
<% end %>
<%= if Event.finished?(@event) do %>
<span class="px-2 py-0.5 text-[10px] font-medium rounded-tl-none rounded-lg bg-gray-100 text-gray-600">
{gettext("Finished")}
</span>
<% end %>
</div>
</div>
<!-- 3-dot Menu -->
<div 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-target={@myself}
class="p-1.5 text-gray-400 hover:text-gray-600"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="currentColor"
viewBox="0 0 24 24"
>
<circle cx="12" cy="5" r="2" />
<circle cx="12" cy="12" r="2" />
<circle cx="12" cy="19" r="2" />
</svg>
</button>
<div
id={"dropdown-menu-#{@event.uuid}"}
phx-hook="Dropdown"
class="hidden absolute right-0 top-8 w-36 rounded-lg shadow-lg bg-white border z-30"
>
{render_dropdown_menu(assigns)}
</div>
</div>
</div>
<!-- Bottom Row: Action Buttons -->
<div class="flex gap-2">
<%= if @event.presentation_file.status == "done" && !Event.finished?(@event) do %>
<a
href={~p"/e/#{@event.code}/manage"}
class="flex items-center justify-center gap-1.5 px-3 py-2 border border-secondary-500 text-secondary-500 rounded-full font-bold text-sm hover:bg-secondary-50 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 rotate-[135deg]"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z"
clip-rule="evenodd"
/>
</svg>
{gettext("Join")}
</a>
<.link
:if={Event.started?(@event) && not @is_leader}
data-confirm={
gettext(
"Are you sure you want to terminate this event? This action cannot be undone."
)
}
phx-value-id={@event.uuid}
phx-click="terminate"
class="flex items-center justify-center gap-1.5 px-3 py-2 bg-gray-100 text-gray-700 rounded-full font-bold text-sm hover:bg-gray-200 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
</svg>
{gettext("Terminé")}
</.link>
<% end %>
<%= if @event.presentation_file.status == "progress" do %>
<div class="flex items-center gap-2 px-3 py-2">
<img src="/images/loading.gif" class="h-5" />
<span class="text-sm text-gray-500">{gettext("Processing...")}</span>
</div>
<% end %>
<%= if @event.presentation_file.status == "fail" do %>
<span class="text-sm text-supporting-red-500 px-3 py-2">{gettext("Error")}</span>
<% end %>
<%= if Event.finished?(@event) do %>
<a
href={~p"/events/#{@event.uuid}/stats"}
class="flex items-center justify-center gap-1.5 px-3 py-2 border border-secondary-500 text-secondary-500 rounded-full font-bold text-sm hover:bg-secondary-50 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 rotate-[135deg]"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z"
clip-rule="evenodd"
/>
</svg>
{gettext("View report")}
</a>
<span class="flex items-center justify-center gap-1.5 px-3 py-2 bg-gray-100 text-gray-700 rounded-full font-bold text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
</svg>
{gettext("Terminé")}
</span>
<% end %>
</div>
</div>
</div>
</li>
"""
end
defp render_dropdown_menu(assigns) do
~H"""
<ul class="py-1">

View File

@@ -1,4 +1,4 @@
<div class="px-6 lg:px-12 pb-12">
<div class="px-4 lg:px-12 pb-12 lg:pb-12 pb-36">
<!-- Event Form Modal -->
<%= if @live_action in [:new, :edit] do %>
<.live_component
@@ -72,8 +72,65 @@
</div>
<!-- Filter Bar and Actions -->
<div class="pt-6 pb-6">
<div class="flex flex-col lg:flex-row items-start lg:items-center justify-between gap-4">
<div class="pt-4 lg:pt-6 pb-4 lg:pb-6">
<!-- Mobile: Search bar + Centered tabs -->
<div class="lg:hidden mb-4">
<form phx-submit="search" phx-change="search" class="relative">
<div class="flex items-center gap-2 bg-white border border-gray-200 rounded-full px-4 h-10">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
type="text"
name="search"
placeholder={gettext("Search")}
class="flex-1 bg-transparent border-none focus:outline-none focus:ring-0 text-sm text-gray-700 placeholder-gray-400"
phx-debounce="300"
/>
</div>
</form>
</div>
<div class="flex lg:hidden items-center justify-center">
<.tabs style={:boxed}>
<:tab
active={@active_tab == "not_expired"}
phx-click="change-tab"
phx-value-tab="not_expired"
>
{gettext("Active")}
</:tab>
<:tab
active={@active_tab == "expired"}
disabled={not @has_expired_events}
phx-click="change-tab"
phx-value-tab="expired"
>
{gettext("Done")}
</:tab>
<:tab
active={@active_tab == "invited"}
disabled={not @has_invited_events}
phx-click="change-tab"
phx-value-tab="invited"
>
{gettext("Shared with you")}
</:tab>
</.tabs>
</div>
<!-- Desktop: Full layout with tabs, view toggle, and action buttons -->
<div class="hidden lg:flex items-center justify-between gap-4">
<!-- Left: Filter Tabs -->
<.tabs style={:boxed}>
<:tab
@@ -104,7 +161,11 @@
<!-- Right: View Toggle & Action Buttons -->
<div class="flex items-center gap-2 flex-wrap">
<!-- View Toggle -->
<div id="view-mode-toggle" phx-hook="ViewModePreference" class="flex items-center gap-1 bg-white border border-gray-200 rounded-full p-1">
<div
id="view-mode-toggle"
phx-hook="ViewModePreference"
class="flex items-center gap-1 bg-white border border-gray-200 rounded-full p-1"
>
<button
phx-click="change-view"
phx-value-view="grid"
@@ -160,7 +221,7 @@
clip-rule="evenodd"
/>
</svg>
<span class="hidden sm:inline">{gettext("Create a quick event")}</span>
<span>{gettext("Create a quick event")}</span>
</button>
<!-- Create Event Button -->
@@ -180,7 +241,7 @@
clip-rule="evenodd"
/>
</svg>
<span class="hidden sm:inline">{gettext("Create an event")}</span>
<span>{gettext("Create an event")}</span>
</.link>
</div>
</div>
@@ -188,9 +249,25 @@
<!-- Events Grid/List -->
<div class="relative">
<!-- Mobile: Always list view -->
<ul role="event-list" id="events-mobile" class="lg:hidden space-y-2.5">
<% current_time = NaiveDateTime.utc_now() %>
<%= for event <- @events do %>
<.live_component
module={ClaperWeb.EventLive.EventCardComponent}
id={"event-mobile-#{event.id}"}
event={event}
current_time={current_time}
is_leader={@active_tab == "invited"}
view_mode="mobile"
/>
<% end %>
</ul>
<!-- Desktop: Grid or List based on view_mode -->
<%= if @view_mode == "grid" do %>
<!-- Grid View -->
<div id="events" class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div id="events" class="hidden lg:grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<% current_time = NaiveDateTime.utc_now() %>
<%= for event <- @events do %>
<.live_component
@@ -205,7 +282,7 @@
</div>
<% else %>
<!-- List View -->
<ul role="event-list" id="events" class="space-y-4">
<ul role="event-list" id="events" class="hidden lg:block space-y-4">
<% current_time = NaiveDateTime.utc_now() %>
<%= for event <- @events do %>
<.live_component
@@ -271,5 +348,47 @@
</div>
<% end %>
</div>
<!-- Mobile Fixed Bottom Action Buttons -->
<div class="lg:hidden fixed bottom-0 left-0 right-0 bg-gradient-to-t from-white from-70% via-white via-85% to-transparent px-4 pt-12 pb-6 z-20">
<div class="flex flex-col gap-2.5">
<button
phx-click="toggle-quick-create"
class="w-full flex items-center justify-center gap-2 h-12 px-4 border border-secondary-500 text-secondary-500 rounded-full font-bold hover:bg-secondary-50 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
<span>{gettext("Create a quick event")}</span>
</button>
<.link
href={~p"/events/new"}
class="w-full flex items-center justify-center gap-2 h-12 px-4 bg-primary-500 text-white rounded-full font-bold hover:bg-primary-600 transition"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z"
clip-rule="evenodd"
/>
</svg>
<span>{gettext("Create an event")}</span>
</.link>
</div>
</div>
<% end %>
</div>

View File

@@ -10,20 +10,63 @@
</div>
</div>
<!-- Main Header -->
<div class="bg-white py-4 px-6 lg:px-12">
<!-- Mobile Header -->
<div class="lg:hidden bg-white py-3 px-4">
<div class="flex items-center justify-between">
<!-- Logo -->
<a href={~p"/events"} class="shrink-0 bg-white p-2 rounded-full shadow-md">
<img src="/images/logo.svg" class="h-4" alt="Claper" />
</a>
<!-- Title -->
<h1 class="text-base font-bold text-secondary-500 capitalize">
{gettext("My events")}
</h1>
<!-- User Menu -->
<div class="dropdown dropdown-end shrink-0 bg-white p-1 shadow-md rounded-full">
<div
tabindex="0"
role="button"
class="flex items-center justify-center bg-gray-100 border border-gray-100 rounded-full p-2 hover:bg-gray-200 transition cursor-pointer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-5 h-5 text-gray-700"
>
<path
fill-rule="evenodd"
d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
clip-rule="evenodd"
/>
</svg>
</div>
<ul
tabindex="0"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 shadow-lg mt-2"
>
{render("_user_menu.html", conn: @conn, user: @user)}
</ul>
</div>
</div>
</div>
<!-- Desktop Header -->
<div class="hidden lg:block bg-white py-4 px-6 lg:px-12">
<div class="flex items-center gap-6">
<!-- Logo -->
<a href={~p"/events"} class="shrink-0 bg-white p-2 rounded-full shadow-md">
<img src="/images/logo.svg" class="h-5" alt="Claper" />
</a>
<!-- Title -->
<!-- Title -->
<h1 class="text-2xl font-bold text-secondary-500 capitalize shrink-0">
{gettext("My events")}
</h1>
<!-- Search Bar -->
<!-- Search Bar -->
<div class="flex-1 max-w-md">
<form phx-submit="search" phx-change="search" class="relative">
<div class="flex items-center gap-2 bg-white border border-gray-200 rounded-full px-4 py-3">
@@ -51,11 +94,11 @@
</div>
</form>
</div>
<!-- Spacer -->
<!-- Spacer -->
<div class="flex-1"></div>
<!-- User Menu -->
<!-- User Menu -->
<div class="dropdown dropdown-end shrink-0 bg-white p-1 shadow-md rounded-full">
<div
tabindex="0"