replace PDP image gallery with scroll-snap carousel

Mobile: swipeable carousel with dot indicators, no lightbox trigger.
Desktop: carousel with thumbnail grid, prev/next arrows, click to
open existing lightbox. Keeps all lightbox appearance and behaviour.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-10 15:33:41 +00:00
parent 1a69736734
commit 8445e9e8b1
4 changed files with 335 additions and 43 deletions

View File

@@ -1134,52 +1134,136 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def product_gallery(assigns) do
~H"""
<div>
<div
class="pdp-main-image-container aspect-square bg-gray-200 mb-4 overflow-hidden relative"
style="border-radius: var(--t-radius-image);"
phx-click={Phoenix.LiveView.JS.exec("data-show", to: "##{@id_prefix}-lightbox")}
>
<img
id={"#{@id_prefix}-main-image"}
src={List.first(@images)}
alt={@product_name}
width="600"
height="600"
class="w-full h-full object-cover"
/>
<div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity bg-black/10">
<div class="w-12 h-12 bg-white/90 rounded-full flex items-center justify-center shadow-lg">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"
/>
</svg>
<div class="pdp-gallery">
<%!-- Image area (relative container for dots + desktop nav) --%>
<div class="relative" style="border-radius: var(--t-radius-image); overflow: hidden;">
<%!-- Scroll-snap carousel (2+ images) or single image --%>
<%= if length(@images) > 1 do %>
<div
id={"#{@id_prefix}-carousel"}
class="pdp-gallery-carousel"
phx-hook="ProductImageScroll"
role="region"
aria-label="Product images"
>
<img
:for={{url, idx} <- Enum.with_index(@images)}
src={url}
alt={"#{@product_name} — image #{idx + 1} of #{length(@images)}"}
class="pdp-carousel-img"
width="600"
height="600"
loading={if idx == 0, do: nil, else: "lazy"}
decoding={if idx == 0, do: nil, else: "async"}
/>
</div>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<%= for {img_url, idx} <- Enum.with_index(@images) do %>
<%!-- Desktop: lightbox click area (transparent, above carousel) --%>
<div
class="pdp-lightbox-click"
phx-click={Phoenix.LiveView.JS.exec("data-show", to: "##{@id_prefix}-lightbox")}
/>
<%!-- Desktop: prev/next arrows (same chevrons as lightbox) --%>
<button
type="button"
class={"aspect-square bg-gray-200 cursor-pointer overflow-hidden pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
class="pdp-nav pdp-nav-prev"
aria-label="Previous image"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:scroll-prev", to: "##{@id_prefix}-carousel")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
<button
type="button"
class="pdp-nav pdp-nav-next"
aria-label="Next image"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:scroll-next", to: "##{@id_prefix}-carousel")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
<% else %>
<div class="pdp-gallery-single">
<%= if @images == [] do %>
<div
class="w-full h-full flex items-center justify-center"
style="color: var(--t-text-tertiary);"
role="img"
aria-label={@product_name}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-12 opacity-40"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0 0 22.5 18.75V5.25A2.25 2.25 0 0 0 20.25 3H3.75A2.25 2.25 0 0 0 1.5 5.25v13.5A2.25 2.25 0 0 0 3.75 21Zm16.5-13.5a1.125 1.125 0 1 1-2.25 0 1.125 1.125 0 0 1 2.25 0Z"
/>
</svg>
</div>
<% else %>
<img
src={List.first(@images)}
alt={@product_name}
width="600"
height="600"
class="w-full h-full object-cover"
/>
<div
class="pdp-lightbox-click"
phx-click={Phoenix.LiveView.JS.exec("data-show", to: "##{@id_prefix}-lightbox")}
/>
<% end %>
</div>
<% end %>
<%!-- Dot indicators (absolute over image, hidden on desktop) --%>
<div
:if={length(@images) > 1}
class="product-image-dots"
aria-hidden="true"
>
<span
:for={{_, idx} <- Enum.with_index(@images)}
class={["product-image-dot", idx == 0 && "product-image-dot-active"]}
/>
</div>
</div>
<%!-- Desktop: thumbnail grid --%>
<div :if={length(@images) > 1} class="pdp-gallery-thumbs">
<%= for {url, idx} <- Enum.with_index(@images) do %>
<button
type="button"
class={"aspect-square bg-gray-200 overflow-hidden pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
style="border-radius: var(--t-radius-image);"
data-index={idx}
aria-label={"View image #{idx + 1} of #{length(@images)}"}
phx-click={
Phoenix.LiveView.JS.set_attribute({"src", img_url}, to: "##{@id_prefix}-main-image")
|> Phoenix.LiveView.JS.set_attribute({"data-current-index", to_string(idx)},
Phoenix.LiveView.JS.dispatch("pdp:scroll-to",
detail: %{index: idx},
to: "##{@id_prefix}-carousel"
)
|> Phoenix.LiveView.JS.set_attribute(
{"data-current-index", to_string(idx)},
to: "##{@id_prefix}-lightbox"
)
|> Phoenix.LiveView.JS.remove_class("pdp-thumbnail-active", to: ".pdp-thumbnail")
|> Phoenix.LiveView.JS.remove_class("pdp-thumbnail-active",
to: ".pdp-thumbnail"
)
|> Phoenix.LiveView.JS.add_class("pdp-thumbnail-active")
}
>
<img
src={img_url}
alt={@product_name}
src={url}
alt=""
width="150"
height="150"
loading="lazy"
@@ -1189,7 +1273,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<% end %>
</div>
<.product_lightbox images={@images} product_name={@product_name} id_prefix={@id_prefix} />
<.product_lightbox
:if={@images != []}
images={@images}
product_name={@product_name}
id_prefix={@id_prefix}
/>
</div>
"""
end