PhoenixKitCatalogue.Web.Components.ProductCard (PhoenixKitCatalogue v0.19.1)

Copy Markdown View Source

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"
/>

Summary

Functions

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.

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

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.

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.

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).

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

Functions

build_fields(item, locale)

@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(assigns)

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(assigns)

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(_)

@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(_)

@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(item, locale)

@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).