# `PhoenixKitCatalogue.Web.Components.ProductCard`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.19.1/lib/phoenix_kit_catalogue/web/components/product_card.ex#L1)

A read-only product card for a catalogue `%Item{}` — opened from the
`ItemPicker` thumbnail or a list's featured-image thumb, and potentially
shown to a CLIENT, not just admins, so it has to stand on its own.

`product_card/1` renders a `<.modal>` whose media area is ONE continuous
swipeable carousel: the item's photos first, then its attached files (a
PDF renders inline, any other file as a tile with an Open action) — swipe
through the photos and just keep going into the files. Below it: the
item's filled scalar fields (SKU, price, unit, description, metadata) and
a compact file list for saving. Slide switching is entirely client-side
(scroll-snap); the only server event left is the close.

Image/file resolution and field extraction (the DB-backed work) live in
the public helpers `resolve_images/1`, `resolve_files/1`, `resolve_name/2`,
and `build_fields/2` so the component itself stays render-only and
testable without a database. `product_card_body/1` is the same content
without the modal shell (the "notpopup" form).

## Usage (from a LiveComponent or LiveView that owns the state)

    <ProductCard.product_card
      id={@id}
      show={@card_open}
      item_name={@card_name}
      images={@card_images}
      fields={@card_fields}
      files={@card_files}
      target={@myself}
      on_close="card_close"
    />

# `build_fields`

```elixir
@spec build_fields(PhoenixKitCatalogue.Schemas.Item.t() | term(), String.t()) :: [
  {String.t(), String.t()}
]
```

Builds the ordered `{label, value}` list of the item's filled,
user-facing scalar fields — SKU, price, unit, description, then
metadata. Empty values are dropped, so the card only shows what is set.

# `product_card`

Renders the product card modal. Pure: every DB-backed value
(`images`, `fields`, `item_name`) is resolved by the caller and passed
in.

Attrs:

  * `:id` (required) — used to derive the modal's DOM id.
  * `:show` (required) — whether the modal is open.
  * `:target` (required) — the `@myself` of the LiveComponent that
    handles `card_select_image` / the close event (the `ItemPicker`).
  * `:item_name` — card title.
  * `:images` — ordered list of `%{uuid, name}` (main image first).
  * `:current_image` — UUID of the image shown large.
  * `:fields` — list of `{label, value}` for the already-filtered,
    non-empty fields.
  * `:on_close` — event pushed to `@target` on close (default
    `"card_close"`).

## Attributes

* `id` (`:string`) (required)
* `show` (`:boolean`) (required)
* `target` (`:any`) (required)
* `item_name` (`:string`) - Defaults to `nil`.
* `images` (`:list`) - Defaults to `[]`.
* `current_image` (`:string`) - Accepted for API compatibility; the carousel starts at the first slide (the featured image is already first) and slides are switched client-side, so this no longer drives the render. Defaults to `nil`.
* `fields` (`:list`) - Defaults to `[]`.
* `files` (`:list`) - Defaults to `[]`.
* `on_close` (`:string`) - Defaults to `"card_close"`.

# `product_card_body`

The card's content without the modal shell — the "notpopup" form, for
embedding the same product view inline (a detail pane, a future product
page). Same attrs as `product_card/1` minus the modal ones.

The media area is ONE continuous swipeable carousel: photos first, then
the attached files (a PDF renders inline, any other file as a tile) — the
client swipes through the photos and just keeps going into the files.
Scroll-snap (daisyUI `carousel`) drives it entirely client-side: native
swipe on touch, arrow buttons on desktop, no server round-trip per slide.

## Attributes

* `target` (`:any`) (required)
* `item_name` (`:string`) - Defaults to `nil`.
* `images` (`:list`) - Defaults to `[]`.
* `current_image` (`:string`) - accepted for API compatibility; unused. Defaults to `nil`.
* `fields` (`:list`) - Defaults to `[]`.
* `files` (`:list`) - Defaults to `[]`.

# `resolve_files`

```elixir
@spec resolve_files(PhoenixKitCatalogue.Schemas.Item.t() | term()) :: [
  %{
    uuid: String.t(),
    name: String.t() | nil,
    size: integer() | nil,
    pdf?: boolean()
  }
]
```

Resolves the item's attached NON-image files (documents, PDFs, …) from
its `files_folder_uuid`, as `%{uuid, name, size, pdf?}` maps. PDFs are
flagged so the card can offer the inline viewer. Nil/blank-safe and
rescued the same way `resolve_images/1` is.

# `resolve_images`

```elixir
@spec resolve_images(PhoenixKitCatalogue.Schemas.Item.t() | term()) :: [
  %{uuid: String.t(), name: String.t() | nil}
]
```

Resolves the ordered gallery images for an item: the main
(`featured_image_uuid`) first, then the remaining image files in the
item's `files_folder_uuid`, de-duplicated. Returns `%{uuid, name}`
maps. Nil/blank-safe and rescued — a missing folder or a Storage
hiccup degrades to just the main image (or `[]` if there is none).

# `resolve_name`

```elixir
@spec resolve_name(PhoenixKitCatalogue.Schemas.Item.t() | term(), String.t()) ::
  String.t() | nil
```

Resolves the item's display name for the given locale (translation, then bare name).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
