Class: Www::ProductCardsComponent

Inherits:
ApplicationComponent show all
Includes:
ActionView::Helpers::TagHelper, ActionView::Helpers::UrlHelper
Defined in:
app/components/www/product_cards_component.rb

Overview

Www::ProductCardsComponent

Renders a responsive grid of product cards. Feed it product-line slugs,
SKUs, or pre-built option hashes. The component resolves product data
(images, links, ratings, pricing) and outputs accessible, SEO-safe markup.

Data inputs (mutually exclusive, checked in this order)
options: Array of option hashes or SKU strings.
skus: Array of SKU strings, or Hash { 'SKU' => { overrides } }.
product_line_slugs: Array of slugs, or Hash { slug => { overrides } }.
Overrides: link, primary_image_id, accessories,
additional_features (extra bullets for this page only).

Section chrome
section_title, section_description, section_icon, section_svg_icon,
group_link, group_link_title

Layout
layout: :vertical (default) or :horizontal_full
row_cols_classes: Override the default Bootstrap grid classes.
row_dom_id: DOM id for the grid container (Turbo targeting).
carousel: Render cards in a Fancyapps carousel instead of a grid.
render_only_cards: Omit wrapper markup (for Turbo Stream appends).

Card display
simple_card: Minimal card (hides description, features, whats-included).
render_schema: Add CollectionPage schema entries (default true).
cta_label: CTA button label (default "View Details").
hide_cta: Hide the CTA button entirely (default false).
open_sections: Expand accordion sections by default (default false).
include_feature_collapse: Show features as collapsible (default true, false in simple_card).
show_page_description: Show short description (default true, false in simple_card).
show_price: Show price in footer (default true).
include_sku: Show SKU under title (default true).
include_whats_included: Show "What's Included?" (default true, false in simple_card).
image_fit: :contain (default, pad + bg-light) or nil (natural sizing).
motion: Per-card fadeInUp entrance animation (default true). Pass
false where the cards are swapped in a Turbo Frame and a
per-card slide-up would read as jumpy (e.g. towel-warmer).

Examples

Simple:
render Www::ProductCardsComponent.new(
simple_card: true,
product_line_slugs: [ProductLineUrls::FLOOR_HEATING_TEMPZONE_FLEX_ROLL]
)

Full featured:
render Www::ProductCardsComponent.new(
product_line_slugs: [ProductLineUrls::FLOOR_HEATING_CONTROL],
include_feature_collapse: true,
open_sections: true,
show_price: true,
include_sku: false
)

Constant Summary collapse

DEFAULT_ICON =

Default icon.

'box'.freeze
RESERVED_OVERRIDE_KEYS =

Per-slug override keys with dedicated handling in #build_options_from_slugs.
Anything else at the slug level is a back-compat { 'SKU' => 'Label' } accessory
map, so a new key has to be listed here or it gets read as accessories.

%w[
  link primary_image_id primary_image_slug accessories compatible_accessories additional_features
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationComponent

#cms_link, #fetch_or_fallback, #image_asset_tag, #image_tag, #number_to_currency, #number_with_delimiter, #post_path, #post_url, #strip_tags

Constructor Details

#initialize(options: nil, product_line_slugs: nil, skus: nil, section_title: nil, section_icon: nil, section_svg_icon: nil, section_description: nil, group_link: nil, group_link_title: nil, simple_card: false, render_schema: true, cta_label: 'View Details', cta_modal_target: nil, row_cols_classes: nil, render_only_cards: false, layout: :vertical, carousel: false, row_dom_id: nil, show_page_description: nil, show_price: true, include_sku: true, include_whats_included: nil, image_fit: :contain, motion: true, hide_cta: false, open_sections: false, include_feature_collapse: nil) ⇒ void

Sets up the component's display flags and defers option resolution to #before_render.

Parameters:

  • options (Array<Hash, String>, nil) (defaults to: nil)

    pre-built option hashes or SKU strings

  • product_line_slugs (Array<String>, Hash{String => Hash}, nil) (defaults to: nil)

    product-line slugs, or a hash of slug => override map

  • skus (Array<String>, Hash{String => Hash}, nil) (defaults to: nil)

    SKU strings, or a hash of 'SKU' => override map

  • section_title (String, nil) (defaults to: nil)

    heading rendered above the grid

  • section_icon (String, nil) (defaults to: nil)

    Font Awesome icon name for the heading (default DEFAULT_ICON); ignored when +section_svg_icon+ is given

  • section_svg_icon (String, nil) (defaults to: nil)

    inline SVG icon markup for the heading; takes precedence over +section_icon+

  • section_description (String, nil) (defaults to: nil)

    blurb rendered under the heading

  • group_link (String, nil) (defaults to: nil)

    URL for a "view all" link in the section header

  • group_link_title (String, nil) (defaults to: nil)

    label for the +group_link+

  • simple_card (Boolean) (defaults to: false)

    render minimal cards (hides description, features, whats-included)

  • render_schema (Boolean) (defaults to: true)

    add CollectionPage schema entries per card

  • cta_label (String) (defaults to: 'View Details')

    label for the card CTA button

  • cta_modal_target (String, nil) (defaults to: nil)

    DOM target that turns the CTA into a modal trigger

  • row_cols_classes (String, nil) (defaults to: nil)

    override for the Bootstrap grid classes

  • render_only_cards (Boolean) (defaults to: false)

    omit wrapper markup (for Turbo Stream appends)

  • layout (Symbol) (defaults to: :vertical)

    :vertical (default) or :horizontal_full

  • carousel (Boolean) (defaults to: false)

    render cards in a Fancyapps carousel instead of a grid

  • row_dom_id (String, nil) (defaults to: nil)

    DOM id for the grid container (Turbo targeting)

  • show_page_description (Boolean, nil) (defaults to: nil)

    show short description (defaults to !simple_card)

  • show_price (Boolean) (defaults to: true)

    show price in the card footer

  • include_sku (Boolean) (defaults to: true)

    show SKU under the title

  • include_whats_included (Boolean, nil) (defaults to: nil)

    show "What's Included?" (defaults to !simple_card)

  • image_fit (Symbol, nil) (defaults to: :contain)

    :contain (pad + bg-light) or nil for natural sizing

  • motion (Boolean) (defaults to: true)

    per-card fadeInUp entrance animation

  • hide_cta (Boolean) (defaults to: false)

    hide the CTA button entirely

  • open_sections (Boolean) (defaults to: false)

    expand accordion sections by default

  • include_feature_collapse (Boolean, nil) (defaults to: nil)

    show features as collapsible (defaults to !simple_card)



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'app/components/www/product_cards_component.rb', line 110

def initialize(options: nil, product_line_slugs: nil, skus: nil,
               section_title: nil,
               section_icon: nil,
               section_svg_icon: nil,
               section_description: nil,
               group_link: nil, group_link_title: nil,
               simple_card: false,
               render_schema: true, cta_label: 'View Details',
               cta_modal_target: nil,
               row_cols_classes: nil,
               render_only_cards: false,
               layout: :vertical,
               carousel: false,
               row_dom_id: nil,
               show_page_description: nil,
               show_price: true,
               include_sku: true,
               include_whats_included: nil,
               image_fit: :contain,
               motion: true,
               hide_cta: false,
               open_sections: false,
               include_feature_collapse: nil)
  @section_title = section_title
  @section_icon = section_svg_icon.present? ? nil : (section_icon || DEFAULT_ICON)
  @section_svg_icon = section_svg_icon
  @section_description = section_description
  @group_link = group_link
  @group_link_title = group_link_title
  @simple_card = simple_card
  @render_schema = render_schema
  @cta_label = cta_label
  @cta_modal_target = cta_modal_target
  @layout = layout.to_sym

  @hide_cta = hide_cta
  @open_sections = open_sections
  @include_feature_collapse = include_feature_collapse.nil? ? !simple_card : include_feature_collapse
  @render_only_cards = render_only_cards

  @row_cols_classes = row_cols_classes || default_row_cols_classes

  @show_page_description = show_page_description.nil? ? !simple_card : show_page_description
  @include_whats_included = include_whats_included.nil? ? !simple_card : include_whats_included
  @include_sku = include_sku
  @show_price = show_price
  @motion = motion
  @image_fit = image_fit

  if @layout == :horizontal_full
    @include_feature_collapse = false
    @open_sections = true
  end

  # Defer resolving options until we have a view context (helpers) available
  @init_options = Array(options).compact.presence
  @init_skus = skus
  # Accept either an Array of slugs or a Hash of { slug => accessory_map }
  @slug_overrides = {}
  if product_line_slugs.is_a?(Hash)
    @init_product_line_slugs = product_line_slugs.keys.map(&:to_s)
    @slug_overrides = product_line_slugs.transform_keys(&:to_s)
  else
    @init_product_line_slugs = Array(product_line_slugs).compact.map(&:to_s)
  end
  @locale = I18n.locale
  @options = []
  @carousel = carousel
  @row_dom_id = row_dom_id

  super()
end

Instance Attribute Details

Returns the value of attribute carousel.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def carousel
  @carousel
end

#cta_labelObject (readonly)

Returns the value of attribute cta_label.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def cta_label
  @cta_label
end

#cta_modal_targetObject (readonly)

Returns the value of attribute cta_modal_target.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def cta_modal_target
  @cta_modal_target
end

Returns the value of attribute group_link.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def group_link
  @group_link
end

Returns the value of attribute group_link_title.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def group_link_title
  @group_link_title
end

#hide_ctaObject (readonly)

Returns the value of attribute hide_cta.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def hide_cta
  @hide_cta
end

#image_fitObject (readonly)

Returns the value of attribute image_fit.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def image_fit
  @image_fit
end

#include_feature_collapseObject (readonly)

Returns the value of attribute include_feature_collapse.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def include_feature_collapse
  @include_feature_collapse
end

#include_skuObject (readonly)

Returns the value of attribute include_sku.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def include_sku
  @include_sku
end

#include_whats_includedObject (readonly)

Returns the value of attribute include_whats_included.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def include_whats_included
  @include_whats_included
end

#layoutObject (readonly)

Returns the value of attribute layout.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def layout
  @layout
end

#motionObject (readonly)

Returns the value of attribute motion.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def motion
  @motion
end

#open_sectionsObject (readonly)

Returns the value of attribute open_sections.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def open_sections
  @open_sections
end

#optionsObject (readonly)

Returns the value of attribute options.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def options
  @options
end

#render_only_cardsObject (readonly)

Returns the value of attribute render_only_cards.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def render_only_cards
  @render_only_cards
end

#render_schemaObject (readonly)

Returns the value of attribute render_schema.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def render_schema
  @render_schema
end

#row_cols_classesObject (readonly)

Returns the value of attribute row_cols_classes.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def row_cols_classes
  @row_cols_classes
end

#row_dom_idObject (readonly)

Returns the value of attribute row_dom_id.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def row_dom_id
  @row_dom_id
end

#section_descriptionObject (readonly)

Returns the value of attribute section_description.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def section_description
  @section_description
end

#section_iconObject (readonly)

Returns the value of attribute section_icon.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def section_icon
  @section_icon
end

#section_svg_iconObject (readonly)

Returns the value of attribute section_svg_icon.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def section_svg_icon
  @section_svg_icon
end

#section_titleObject (readonly)

Returns the value of attribute section_title.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def section_title
  @section_title
end

#show_page_descriptionObject (readonly)

Returns the value of attribute show_page_description.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def show_page_description
  @show_page_description
end

#show_priceObject (readonly)

Returns the value of attribute show_price.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def show_price
  @show_price
end

#simple_cardObject (readonly)

Returns the value of attribute simple_card.



64
65
66
# File 'app/components/www/product_cards_component.rb', line 64

def simple_card
  @simple_card
end

Instance Method Details

#before_rendervoid

This method returns an undefined value.

Resolves the deferred inputs (options, SKUs, or slugs) into card option hashes.
Runs before render so view helpers are available for link/image resolution.



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'app/components/www/product_cards_component.rb', line 247

def before_render
  resolved_options =
    if @init_options.present?
      normalize_options(@init_options, @locale)
    elsif @init_skus.is_a?(Hash) && @init_skus.any?
      normalize_sku_hash(@init_skus, @locale)
    elsif Array(@init_skus).compact.any?
      normalize_options(Array(@init_skus).compact, @locale)
    else
      build_options_from_slugs(@init_product_line_slugs, @locale)
    end

  @options = resolved_options
  @options = Array(@options).map { |opt| opt.merge(open_sections: true) } if @open_sections
end

#card_component_for(option, index:, motion_override: motion) ⇒ Www::ProductCardComponent

Returns the configured per-card component.

Parameters:

  • option (Hash)

    product card option data, passed through to Www::ProductCardComponent

  • index (Integer)

    card position within the grid or carousel

  • motion_override (Boolean) (defaults to: motion)

    per-card override for the entrance animation

Options Hash (option):

  • :open_sections (Boolean, nil)

    per-card override for expanding accordion sections by default

Returns:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'app/components/www/product_cards_component.rb', line 212

def card_component_for(option, index:, motion_override: motion)
  Www::ProductCardComponent.new(
    option: option,
    index: index,
    simple_card: simple_card,
    include_sku: include_sku,
    include_feature_collapse: include_feature_collapse,
    include_whats_included: include_whats_included,
    show_page_description: show_page_description,
    show_price: show_price,
    hide_cta: hide_cta,
    cta_label: cta_label,
    cta_modal_target: cta_modal_target,
    open_sections: option[:open_sections] || open_sections,
    render_schema: render_schema,
    layout: layout,
    motion: motion_override,
    image_fit: image_fit
  )
end

Fancyapps carousel configuration sized to the resolved card count.

Returns:

  • (Hash)

    carousel options including responsive breakpoints



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'app/components/www/product_cards_component.rb', line 266

def carousel_config
  count = @options.size
  # Breakpoints mirror CardGridComponent (posts/videos) so product cards
  # maintain the same visual rhythm across all content carousels on a page.
  {
    type:       'slide',
    perPage:    [4, count].min,
    perMove:    1,
    gap:        '1rem',
    pagination: true,
    arrows:     true,
    drag:       true,
    padding:    count > 4 ? { right: '4%' } : { right: '0' },
    breakpoints: {
      1399 => { perPage: [4, count].min, padding: count > 4 ? { right: '4%' } : { right: '0' } },
      1199 => { perPage: [3, count].min, padding: count > 3 ? { right: '5%' } : { right: '0' } },
      991  => { perPage: [3, count].min, padding: count > 3 ? { right: '5%' } : { right: '0' } },
      767  => { perPage: [2, count].min, padding: count > 2 ? { right: '8%' } : { right: '0' } },
      575  => { perPage: 1,              padding: count > 1 ? { right: '18%' } : { right: '0' }, arrows: false }
    }
  }
end

#catalog_card_image_asset_slug(presenter) ⇒ String?

Looks up the card image asset slug for a catalog presenter.

Parameters:

  • presenter (Object)

    presenter to extract the card image slug from

Returns:

  • (String, nil)

    the image asset slug, or nil for non-catalog presenters or items without a record



237
238
239
240
241
# File 'app/components/www/product_cards_component.rb', line 237

def catalog_card_image_asset_slug(presenter)
  return nil unless presenter.is_a?(Www::ProductCatalogPresenter) && presenter.item

  presenter.image_asset_slug_for_card
end

#resolve_option(option) ⇒ Hash?

Returns the option merged with resolved catalog data, or nil when the SKU is not found.

Parameters:

  • option (Hash)

    product card option data

Options Hash (option):

  • :sku (String, nil)

    SKU to resolve into catalog data when no presenter is supplied

  • :presenter (Www::ProductCatalogPresenter, nil)

    pre-built presenter; when blank, resolved from :sku

Returns:

  • (Hash, nil)

    the option merged with resolved catalog data, or nil when the SKU is not found



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'app/components/www/product_cards_component.rb', line 187

def resolve_option(option)
  return option unless option[:sku].present? && option[:presenter].blank?

  record = ViewProductCatalog.find_by(item_sku: option[:sku], catalog_id: Catalog.locale_to_catalog_id(I18n.locale))
  unless record
    Rails.logger.warn "[ProductCardsComponent] SKU not found in catalog: #{option[:sku]}"
    return nil
  end

  presenter = Www::ProductCatalogPresenter.new(record)
  {
    title: presenter.title,
    link: presenter.url,
    image_asset_id: catalog_card_image_asset_slug(presenter),
    review_data: presenter.product_review_info,
    presenter: presenter,
    page_description: presenter.page_description
  }.merge(option)
end