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

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

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 =
'box'

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', 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, hide_cta: false, open_sections: false, include_feature_collapse: nil) ⇒ ProductCardsComponent

Returns a new instance of ProductCardsComponent.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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
# File 'app/components/www/product_cards_component.rb', line 66

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',
               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,
               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
  @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 = true
  @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.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def carousel
  @carousel
end

#cta_labelObject (readonly)

Returns the value of attribute cta_label.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def cta_label
  @cta_label
end

Returns the value of attribute group_link.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def group_link
  @group_link
end

Returns the value of attribute group_link_title.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def group_link_title
  @group_link_title
end

#hide_ctaObject (readonly)

Returns the value of attribute hide_cta.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def hide_cta
  @hide_cta
end

#image_fitObject (readonly)

Returns the value of attribute image_fit.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def image_fit
  @image_fit
end

#include_feature_collapseObject (readonly)

Returns the value of attribute include_feature_collapse.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def include_feature_collapse
  @include_feature_collapse
end

#include_skuObject (readonly)

Returns the value of attribute include_sku.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def include_sku
  @include_sku
end

#include_whats_includedObject (readonly)

Returns the value of attribute include_whats_included.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def include_whats_included
  @include_whats_included
end

#layoutObject (readonly)

Returns the value of attribute layout.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def layout
  @layout
end

#motionObject (readonly)

Returns the value of attribute motion.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def motion
  @motion
end

#open_sectionsObject (readonly)

Returns the value of attribute open_sections.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def open_sections
  @open_sections
end

#optionsObject (readonly)

Returns the value of attribute options.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def options
  @options
end

#render_only_cardsObject (readonly)

Returns the value of attribute render_only_cards.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def render_only_cards
  @render_only_cards
end

#render_schemaObject (readonly)

Returns the value of attribute render_schema.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def render_schema
  @render_schema
end

#row_cols_classesObject (readonly)

Returns the value of attribute row_cols_classes.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def row_cols_classes
  @row_cols_classes
end

#row_dom_idObject (readonly)

Returns the value of attribute row_dom_id.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def row_dom_id
  @row_dom_id
end

#section_descriptionObject (readonly)

Returns the value of attribute section_description.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def section_description
  @section_description
end

#section_iconObject (readonly)

Returns the value of attribute section_icon.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def section_icon
  @section_icon
end

#section_svg_iconObject (readonly)

Returns the value of attribute section_svg_icon.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def section_svg_icon
  @section_svg_icon
end

#section_titleObject (readonly)

Returns the value of attribute section_title.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def section_title
  @section_title
end

#show_page_descriptionObject (readonly)

Returns the value of attribute show_page_description.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def show_page_description
  @show_page_description
end

#show_priceObject (readonly)

Returns the value of attribute show_price.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def show_price
  @show_price
end

#simple_cardObject (readonly)

Returns the value of attribute simple_card.



58
59
60
# File 'app/components/www/product_cards_component.rb', line 58

def simple_card
  @simple_card
end

Instance Method Details

#before_renderObject



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/components/www/product_cards_component.rb', line 182

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) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'app/components/www/product_cards_component.rb', line 156

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,
    open_sections: option[:open_sections] || open_sections,
    render_schema: render_schema,
    layout: layout,
    motion: motion_override,
    image_fit: image_fit
  )
end


198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'app/components/www/product_cards_component.rb', line 198

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.
  defaults = {
    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 }
    }
  }
  defaults
end

#catalog_card_image_asset_slug(presenter) ⇒ Object



176
177
178
179
180
# File 'app/components/www/product_cards_component.rb', line 176

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) ⇒ Object



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'app/components/www/product_cards_component.rb', line 136

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