feat(elixir): add component list focus mode

This commit is contained in:
Schuwi
2025-09-17 17:57:40 +02:00
parent 8848986953
commit b6e137632a

View File

@@ -35,6 +35,7 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|> assign(:form, nil)
|> assign(:show_image_modal, false)
|> assign(:modal_image_url, nil)
|> assign(:focused_component_id, nil)
|> allow_upload(:image,
accept: ~w(.jpg .jpeg .png .gif),
max_entries: 1,
@@ -207,6 +208,19 @@ defmodule ComponentsElixirWeb.ComponentsLive do
|> assign(:modal_image_url, nil)}
end
def handle_event("toggle_focus", %{"id" => id}, socket) do
component_id = String.to_integer(id)
new_focused_id =
if socket.assigns.focused_component_id == component_id do
nil # Unfocus if clicking on the same component
else
component_id # Focus on the new component
end
{:noreply, assign(socket, :focused_component_id, new_focused_id)}
end
def handle_event("prevent_close", _params, socket) do
{:noreply, socket}
end
@@ -675,26 +689,14 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<div class="bg-base-100 shadow overflow-hidden sm:rounded-md">
<ul class="divide-y divide-base-300" id="components-list" phx-update="replace">
<%= for component <- @components do %>
<li id={"component-#{component.id}"} class="px-6 py-6 hover:bg-base-200 min-h-[6rem]">
<div class="flex items-start justify-between min-h-[5rem]">
<!-- Component Image -->
<div class="flex-shrink-0 mr-6 h-20 w-20 grid place-items-center">
<%= if component.image_filename do %>
<button phx-click="show_image" phx-value-url={"/user_generated/uploads/images/#{component.image_filename}"} class="hover:opacity-75 transition-opacity block">
<img src={"/user_generated/uploads/images/#{component.image_filename}"} alt={component.name} class="max-h-20 max-w-20 rounded-md object-contain cursor-pointer block" />
</button>
<% else %>
<div class="h-20 w-20 rounded-md bg-base-200 flex items-center justify-center">
<.icon name="hero-cube-transparent" class="h-10 w-10 text-base-content/50" />
</div>
<% end %>
</div>
<div class="flex-1 min-w-0">
<!-- Top row: Name and Category -->
<li id={"component-#{component.id}"} class={"px-6 py-6 hover:bg-base-200 #{if @focused_component_id == component.id, do: "bg-base-50 border-l-4 border-primary", else: "cursor-pointer"}"} phx-click={if @focused_component_id != component.id, do: "toggle_focus", else: nil} phx-value-id={component.id}>
<%= if @focused_component_id == component.id do %>
<!-- Expanded/Focused View -->
<div class="space-y-6">
<!-- Header with name, category, and close button -->
<div class="flex items-start justify-between">
<div class="flex items-center min-w-0 flex-1">
<p class="text-sm font-medium text-primary truncate">
<h3 class="text-lg font-semibold text-primary select-text">
<%= if component.datasheet_url do %>
<a
href={component.datasheet_url}
@@ -706,91 +708,268 @@ defmodule ComponentsElixirWeb.ComponentsLive do
<% else %>
{component.name}
<% end %>
</p>
</h3>
<%= if component.datasheet_url do %>
<span class="ml-2 text-primary" title="Datasheet available">📄</span>
<% end %>
</div>
<div class="ml-4 flex-shrink-0">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
<div class="ml-4 flex items-center space-x-3">
<span class="px-3 py-1 text-sm font-medium rounded-full bg-green-100 text-green-800">
{component.category.name}
</span>
<!-- Close/Collapse button -->
<button
phx-click="toggle_focus"
phx-value-id={component.id}
class="p-1 rounded-full hover:bg-base-200 text-base-content/60 hover:text-base-content transition-colors"
title="Collapse"
>
<.icon name="hero-x-mark" class="w-5 h-5" />
</button>
</div>
</div>
<!-- Middle row: Description -->
<%= if component.description do %>
<div class="mt-1">
<p class="text-sm text-base-content/70 line-clamp-2">
{component.description}
</p>
<!-- Content area with image and details -->
<div class="flex gap-6">
<!-- Large Image -->
<div class="flex-shrink-0">
<%= if component.image_filename do %>
<button
phx-click="show_image"
phx-value-url={"/user_generated/uploads/images/#{component.image_filename}"}
class="hover:opacity-75 transition-opacity block"
>
<img
src={"/user_generated/uploads/images/#{component.image_filename}"}
alt={component.name}
class="h-48 w-48 rounded-lg object-contain cursor-pointer border border-base-300"
/>
</button>
<% else %>
<div class="h-48 w-48 rounded-lg bg-base-200 flex items-center justify-center border border-base-300">
<.icon name="hero-cube-transparent" class="h-20 w-20 text-base-content/50" />
</div>
<% end %>
</div>
<% end %>
<!-- Bottom row: Metadata -->
<div class="mt-2 grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 text-sm text-base-content/60">
<%= if component.storage_location do %>
<div class="flex items-center min-w-0">
<.icon name="hero-map-pin" class="w-4 h-4 mr-1 text-base-content/50 flex-shrink-0" />
<span class="font-medium">Location:</span>
<span class="ml-1 truncate">{storage_location_display_name(component.storage_location)}</span>
<!-- Details -->
<div class="flex-1 space-y-4 select-text">
<!-- Full Description -->
<%= if component.description do %>
<div>
<h4 class="text-sm font-medium text-base-content mb-2">Description</h4>
<p class="text-sm text-base-content/70 leading-relaxed">
{component.description}
</p>
</div>
<% end %>
<!-- Metadata Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<%= if component.storage_location do %>
<div class="flex items-start">
<.icon name="hero-map-pin" class="w-4 h-4 mr-2 text-base-content/50 mt-0.5 flex-shrink-0" />
<div>
<span class="font-medium text-base-content">Location:</span>
<div class="text-base-content/70">{storage_location_display_name(component.storage_location)}</div>
</div>
</div>
<% end %>
<div class="flex items-center">
<.icon name="hero-cube" class="w-4 h-4 mr-2 text-base-content/50" />
<span class="font-medium text-base-content">Count:</span>
<span class="ml-1 text-base-content/70">{component.count}</span>
</div>
<div class="flex items-start">
<.icon name="hero-calendar" class="w-4 h-4 mr-2 text-base-content/50 mt-0.5 flex-shrink-0" />
<div>
<span class="font-medium text-base-content">Entry Date:</span>
<div class="text-base-content/70">{Calendar.strftime(component.inserted_at, "%B %d, %Y")}</div>
</div>
</div>
<%= if @sort_criteria == "all" or @sort_criteria == "id" do %>
<div class="flex items-center">
<.icon name="hero-hashtag" class="w-4 h-4 mr-2 text-base-content/50" />
<span class="font-medium text-base-content">ID:</span>
<span class="ml-1 text-base-content/70">{component.id}</span>
</div>
<% end %>
<%= if component.keywords do %>
<div class="flex items-start">
<.icon name="hero-tag" class="w-4 h-4 mr-2 text-base-content/50 mt-0.5" />
<div>
<span class="font-medium text-base-content">Keywords:</span>
<div class="text-base-content/70">{component.keywords}</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end items-center space-x-2 pt-4 border-t border-base-300">
<button
phx-click="increment_count"
phx-value-id={component.id}
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" />
Add
</button>
<button
phx-click="decrement_count"
phx-value-id={component.id}
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500"
>
<.icon name="hero-minus" class="w-4 h-4 mr-1" />
Remove
</button>
<button
phx-click="show_edit_form"
phx-value-id={component.id}
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<.icon name="hero-pencil" class="w-4 h-4 mr-1" />
Edit
</button>
<button
phx-click="delete_component"
phx-value-id={component.id}
data-confirm="Are you sure you want to delete this component?"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
<.icon name="hero-trash" class="w-4 h-4 mr-1" />
Delete
</button>
</div>
</div>
<% else %>
<!-- Compact/Normal View -->
<div class="flex items-start justify-between min-h-[5rem]">
<!-- Component Image -->
<div class="flex-shrink-0 mr-6 h-20 w-20 grid place-items-center">
<%= if component.image_filename do %>
<button
phx-click="show_image"
phx-value-url={"/user_generated/uploads/images/#{component.image_filename}"}
class="hover:opacity-75 transition-opacity block"
>
<img src={"/user_generated/uploads/images/#{component.image_filename}"} alt={component.name} class="max-h-20 max-w-20 rounded-md object-contain cursor-pointer block" />
</button>
<% else %>
<div class="h-20 w-20 rounded-md bg-base-200 flex items-center justify-center">
<.icon name="hero-cube-transparent" class="h-10 w-10 text-base-content/50" />
</div>
<% end %>
<div class="flex items-center">
<.icon name="hero-cube" class="w-4 h-4 mr-1 text-base-content/50" />
<span class="font-medium">Count:</span>
<span class="ml-1">{component.count}</span>
</div>
<div class="flex-1 min-w-0">
<!-- Top row: Name and Category -->
<div class="flex items-start justify-between">
<div class="flex items-center min-w-0 flex-1">
<p class="text-sm font-medium text-primary truncate">
<%= if component.datasheet_url do %>
<a
href={component.datasheet_url}
target="_blank"
class="hover:text-primary/80"
>
{component.name}
</a>
<% else %>
{component.name}
<% end %>
</p>
<%= if component.datasheet_url do %>
<span class="ml-2 text-primary" title="Datasheet available">📄</span>
<% end %>
</div>
<div class="ml-4 flex-shrink-0">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
{component.category.name}
</span>
</div>
</div>
<%= if @sort_criteria == "all" or @sort_criteria == "id" do %>
<!-- Middle row: Description -->
<%= if component.description do %>
<div class="mt-1">
<p class="text-sm text-base-content/70 line-clamp-2">
{component.description}
</p>
</div>
<% end %>
<!-- Bottom row: Metadata -->
<div class="mt-2 grid grid-cols-1 sm:grid-cols-3 gap-x-4 gap-y-1 text-sm text-base-content/60">
<%= if component.storage_location do %>
<div class="flex items-center min-w-0">
<.icon name="hero-map-pin" class="w-4 h-4 mr-1 text-base-content/50 flex-shrink-0" />
<span class="font-medium">Location:</span>
<span class="ml-1 truncate">{storage_location_display_name(component.storage_location)}</span>
</div>
<% end %>
<div class="flex items-center">
<.icon name="hero-hashtag" class="w-4 h-4 mr-1 text-base-content/50" />
<span class="font-medium">ID:</span>
<span class="ml-1">{component.id}</span>
<.icon name="hero-cube" class="w-4 h-4 mr-1 text-base-content/50" />
<span class="font-medium">Count:</span>
<span class="ml-1">{component.count}</span>
</div>
<%= if @sort_criteria == "all" or @sort_criteria == "id" do %>
<div class="flex items-center">
<.icon name="hero-hashtag" class="w-4 h-4 mr-1 text-base-content/50" />
<span class="font-medium">ID:</span>
<span class="ml-1">{component.id}</span>
</div>
<% end %>
</div>
<!-- Keywords row -->
<%= if component.keywords do %>
<div class="mt-2">
<p class="text-xs text-base-content/50">
<span class="font-medium">Keywords:</span> {component.keywords}
</p>
</div>
<% end %>
</div>
<!-- Keywords row -->
<%= if component.keywords do %>
<div class="mt-2">
<p class="text-xs text-base-content/50">
<span class="font-medium">Keywords:</span> {component.keywords}
</p>
</div>
<% end %>
<div class="ml-5 flex-shrink-0 flex items-center space-x-2">
<button
phx-click="increment_count"
phx-value-id={component.id}
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<.icon name="hero-plus" class="w-4 h-4" />
</button>
<button
phx-click="decrement_count"
phx-value-id={component.id}
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500"
>
<.icon name="hero-minus" class="w-4 h-4" />
</button>
<button
phx-click="show_edit_form"
phx-value-id={component.id}
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<.icon name="hero-pencil" class="w-4 h-4" />
</button>
<button
phx-click="delete_component"
phx-value-id={component.id}
data-confirm="Are you sure you want to delete this component?"
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
</div>
</div>
<div class="ml-5 flex-shrink-0 flex items-center space-x-2">
<button
phx-click="increment_count"
phx-value-id={component.id}
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<.icon name="hero-plus" class="w-4 h-4" />
</button>
<button
phx-click="decrement_count"
phx-value-id={component.id}
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-yellow-600 hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500"
>
<.icon name="hero-minus" class="w-4 h-4" />
</button>
<button
phx-click="show_edit_form"
phx-value-id={component.id}
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<.icon name="hero-pencil" class="w-4 h-4" />
</button>
<button
phx-click="delete_component"
phx-value-id={component.id}
data-confirm="Are you sure you want to delete this component?"
class="inline-flex items-center p-1 border border-transparent rounded-full shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
</div>
</div>
<% end %>
</li>
<% end %>
</ul>