Class: Www::ProductCompareTableComponent

Inherits:
ApplicationComponent show all
Defined in:
app/components/www/product_compare_table_component.rb

Overview

Www::ProductCompareTableComponent

Renders a responsive product comparison table with products displayed in columns
and features listed on the left side. Supports mobile swiping and responsive
column display from 1 column on mobile to 5 columns on largest viewports.

Constructor parameters (keyword args)

  • skus: Array of SKUs to compare
  • features: Array of feature keys to display (optional, will auto-detect if not provided)
  • section_title: Optional title for the comparison section
  • section_description: Optional description for the comparison section

Features are automatically detected from the first product if not provided.
Each feature is displayed as a row with checkmarks or X marks for each product.

Constant Summary collapse

SKU_FEATURES =

Feature definitions per SKU - much easier to maintain than nested case statements

{
  # Sensors are never included with standalone controls — they are sold separately.
  # Use :aerial, :slab_oj, or :slab_eti symbols (see ADD_ON_PRODUCTS) so the
  # template renders a brand-correct product link instead of a checkmark.
  # Brand pairing rule: ETI controls (SC-MZ-TOUCH, SCP-120, SCA-DUAL) take the
  # ETI slab sensor (SLAB-SS / ETOG-55). The OJ economy control (SCE-120) takes
  # the OJ slab kit (SCE-SLAB-SS-KIT / ETOG-56 + housing). The aerial sensor
  # (AIR-SS-2) is brand-agnostic across all four automatic controls.
  'SC-MZ-TOUCH' => {
    'automatic' => true,
    'moisture_sensor' => :aerial,
    'air_temp_sensor' => :aerial,
    'slab_temp_sensor' => :slab_eti,
    'digital_temp_readout' => true,
    'adjust_after_run_time' => true,
    'multi_zone_capability' => true,
    'max_load' => 'N/A max load'
  },
  'SCP-120' => {
    'automatic' => true,
    'outdoor_rated' => true,
    'moisture_sensor' => :aerial,
    'air_temp_sensor' => :aerial,
    'slab_temp_sensor' => :slab_eti,
    'adjust_after_run_time' => true,
    'max_load' => '24 amps max load'
  },
  'SCA-DUAL' => {
    'automatic' => true,
    'outdoor_rated' => true,
    'moisture_sensor' => :aerial,
    'air_temp_sensor' => :aerial,
    'slab_temp_sensor' => :slab_eti, # supports "pavement" sensor per product features
    'adjust_after_run_time' => true,
    'max_load' => '30 amps max load'
  },
  'SCE-120' => {
    'automatic' => true,
    'moisture_sensor' => :aerial,
    'air_temp_sensor' => :aerial,
    'slab_temp_sensor' => :slab_oj,
    'digital_temp_readout' => true,
    'adjust_after_run_time' => true,
    'max_load' => '3 x 16 amps max load'
  },
  'SCV-DUAL' => {
    'automatic' => true,
    'outdoor_rated' => true,
    'moisture_sensor' => true,   # built-in snow & temperature sensor
    'air_temp_sensor' => true,   # built-in snow & temperature sensor
    'adjust_after_run_time' => true,
    'max_load' => '16 amps max load'
  },
  'SCM-DUAL' => {
    'timer' => true,
    'max_load' => '20 Amp, 120 VAC, 50/60 Hz; 10 Amp, 240 VAC, 50/60 Hz; 10 Amp, 277 VAC, 50/60 Hz amps max load'
  },
  'SCW-120-15A' => {
    'control_remotely_via_iphone_android_or_web_app' => true,
    'max_load' => '15A @ 120V amps max load'
  },
  # ── Panel & Towel Warmer Controls ───────────────────────────────────────
  'GK16-30090-0006' => {
    'countdown_timer'  => true,    # 4 preset durations: 4 hr, 2 hr, 1 hr, 30 min
    'programmable'     => false,
    'schedules'        => false,
    'wifi'             => false,
    'voice_control'    => false,
    'remote_support'   => false,
    'display'          => 'None',
    'max_load'         => '15A @ 120V'
  },
  'GK16-30090-0002' => {
    'countdown_timer'  => false,
    'programmable'     => true,    # astronomical clock + up to 50 on/off events
    'schedules'        => 'Up to 50 on/off events',
    'wifi'             => false,
    'voice_control'    => false,
    'remote_support'   => false,
    'display'          => 'Backlit LCD',
    'max_load'         => '15A @ 120V'
  },
  'GK16-30090-0003' => {
    'countdown_timer'  => false,
    'programmable'     => true,    # schedule via My Leviton app
    'schedules'        => 'Via My Leviton app',
    'wifi'             => true,
    'voice_control'    => true,    # Alexa, Google, Apple HomeKit/Siri, SmartThings, IFTTT
    'remote_support'   => false,
    'display'          => 'None',
    'max_load'         => '15A @ 120V'
  },
  # ── Floor Heating Thermostats ────────────────────────────────────────────
  'UWG5-4999-WY' => {
    'countdown_timer'       => false,
    'wifi'                  => true,
    'remote_support'        => true,
    'voice_control'         => true,
    'programmable'          => true,
    'schedules'             => 'Up to 10 schedules',
    'floor_sensor_included' => true,
    'display'               => 'LED touch',
    'max_load'              => '15A @ 120/240V'
  },
  'UTN5-4999' => {
    'countdown_timer'       => false,
    'wifi'                  => false,
    'remote_support'        => false,
    'voice_control'         => false,
    'programmable'          => false,
    'schedules'             => 'Set-and-hold',
    'floor_sensor_included' => true,
    'display'               => 'LED touch',
    'max_load'              => '15A @ 120/240V'
  },
  'UDG4-4999' => {
    'countdown_timer'       => false,
    'wifi'                  => false,
    'remote_support'        => false,
    'voice_control'         => false,
    'programmable'          => true,
    'schedules'             => 'Up to 6 per day',
    'floor_sensor_included' => true,
    'display'               => '3.5" color touchscreen',
    'max_load'              => '15A @ 120/240V'
  },
  # Canadian variant of nSpire Touch — same features as UDG4-4999
  'UDG4-4999-WY' => {
    'countdown_timer'       => false,
    'wifi'                  => false,
    'remote_support'        => false,
    'voice_control'         => false,
    'programmable'          => true,
    'schedules'             => 'Up to 6 per day',
    'floor_sensor_included' => true,
    'display'               => '3.5" color touchscreen',
    'max_load'              => '15A @ 120/240V'
  },
  # nJoin Power Module — extends thermostat capacity for high-load zones.
  # Not a standalone thermostat: schedules and sensor come from the paired thermostat.
  'USG5-4000' => {
    'countdown_timer'       => false,
    'wifi'                  => false,
    'remote_support'        => false,
    'voice_control'         => false,
    'programmable'          => false,
    'schedules'             => 'Via paired thermostat',
    'floor_sensor_included' => false,
    'display'               => 'None',
    'max_load'              => '15A @ 120/208/240V'
  },
  # Canadian variant — identical specs to USG5-4000
  'USG-4000' => {
    'countdown_timer'       => false,
    'wifi'                  => false,
    'remote_support'        => false,
    'voice_control'         => false,
    'programmable'          => false,
    'schedules'             => 'Via paired thermostat',
    'floor_sensor_included' => false,
    'display'               => 'None',
    'max_load'              => '15A @ 120/208/240V'
  },
  # Pipe Freeze Protection Controls (also used in roof and gutter deicing)
  'AIR-STAT' => {
    # Snow melting features
    'automatic' => true,
    'outdoor_rated' => true,
    'moisture_sensor' => false,
    'air_temp_sensor' => true,
    'slab_temp_sensor' => false,
    'digital_temp_readout' => false,
    'adjust_after_run_time' => false,
    'timer' => false,
    'control_remotely_via_iphone_android_or_web_app' => false,
    # Pipe freeze protection features
    'included_sensor' => '1 capillary bulb, 3\' lead',
    'temperature_setpoint' => '40° F',
    'indicators' => 'None',
    'built_in_gfi' => 'None',
    'waterproofing' => 'NEMA 4X',
    'area_of_use' => 'Non-hazardous locations',
    'max_load' => '22 amps max load'
  },
  'PT-ECONOMY' => {
    'included_sensor' => '1 Sensor (20\' lead)',
    'temperature_setpoint' => '30 °F, 38 °F, 45 °F, or 50 °F',
    'indicators' => 'LED Indicators',
    'built_in_gfi' => 'Self-test GFEP 30mA',
    'waterproofing' => 'NEMA 4X',
    'area_of_use' => 'Non-hazardous locations',
    'max_load' => '30 amps max load'
  },
  'PT-SINGLE' => {
    'included_sensor' => '100k ohms, 20\' lead',
    'temperature_setpoint' => 'Adjustable -99.9 °F to 999 °F',
    'indicators' => '2.7" Display',
    'built_in_gfi' => 'Self-test GFEP (Adjustable 1-300mA)',
    'waterproofing' => 'NEMA 4X',
    'area_of_use' => 'Non-hazardous locations',
    'max_load' => '30 amps max load'
  },
  'PT-DUAL' => {
    'included_sensor' => '2 x 100k ohms, 20\' lead',
    'temperature_setpoint' => 'Adjustable -99.9 °F to 999 °F',
    'indicators' => '2.7" Display',
    'built_in_gfi' => 'Self-test GFEP (Adjustable 1-300mA)',
    'waterproofing' => 'NEMA 4X',
    'area_of_use' => 'Non-hazardous locations',
    'max_load' => '2 x 30 amps max load'
  },
  # ── Snow Melting Sensors ─────────────────────────────────────────────────
  # SCE-SLAB-SS-KIT is the OJ slab kit (ETOG-56 + housing) — only pairs with
  # the SCE-120 OJ economy control. SLAB-SS is the ETI slab sensor (ETOG-55) —
  # pairs with the ETI controls (SC-MZ-TOUCH, SCP-120, SCA-DUAL). AIR-SS-2 is
  # brand-agnostic and pairs with all four automatic controls.
  'AIR-SS-2' => {
    'sensor_installation'    => 'Mounted above ground on post or wall',
    'detects_precipitation'  => true,
    'measures_air_temp'      => true,
    'measures_slab_temp'     => false,
    'retrofit_compatible'    => true,
    'compatible_controls'    => 'SC-MZ-TOUCH, SCP-120, SCA-DUAL, SCE-120'
  },
  'SLAB-SS' => {
    'sensor_installation'    => 'Embedded in concrete during pour',
    'detects_precipitation'  => false,
    'measures_air_temp'      => false,
    'measures_slab_temp'     => true,
    'retrofit_compatible'    => false,
    'compatible_controls'    => 'SC-MZ-TOUCH, SCP-120, SCA-DUAL'
  },
  'SCE-SLAB-SS-KIT' => {
    'sensor_installation'    => 'Embedded in concrete during pour',
    'detects_precipitation'  => false,
    'measures_air_temp'      => false,
    'measures_slab_temp'     => true,
    'retrofit_compatible'    => false,
    'compatible_controls'    => 'SCE-120'
  }
}.freeze
ADD_ON_PRODUCTS =

Products that can be added to extend a control's capabilities.
The symbol keys are used as feature values in SKU_FEATURES to signal
"this feature is supported but requires a separate purchase".
anchor: scrolls to the sensors comparison section on the same page.

Slab sensors are brand-paired: :slab_oj points at the SCE-120-compatible OJ
kit (SCE-SLAB-SS-KIT / ETOG-56), and :slab_eti points at the ETI sensor
(SLAB-SS / ETOG-55) used by SC-MZ-TOUCH, SCP-120, and SCA-DUAL.

{
  aerial:   { sku: 'AIR-SS-2',        label: 'Aerial Sensor',    anchor: '#sensors' },
  slab_oj:  { sku: 'SCE-SLAB-SS-KIT', label: 'Slab Sensor Kit',  anchor: '#sensors' },
  slab_eti: { sku: 'SLAB-SS',         label: 'Slab Sensor',      anchor: '#sensors' }
}.freeze
SENSOR_FEATURE_KEYS =

Feature keys that represent physical sensors — these get badge treatment
("Included" or "Pair with…") instead of a plain checkmark/X.

%w[moisture_sensor air_temp_sensor slab_temp_sensor].freeze
BEST_FOR =

"Best for" taglines shown in the comparison table header, below the product title.
Authored specifically for the comparison context — more decision-oriented than
generic DB short_description copy. Takes precedence over item.short_description.
Floor heating thermostats carry full plain-language "best for…" use-case
sentences (approved copy from sell sheet 5765) so non-technical shoppers can
tell at a glance which control suits how they live.

{
    # ── Floor Heating Thermostats ──────────────────────────────────────────────
    'UTN5-4999'       => 'Best for users who want to maintain consistent temperature and adjust it when needed from the thermostat.',
    'UDG4-4999'       => 'Best for users who prefer full control and setup directly on the thermostat via an intuitive touchscreen display.',
    'UDG4-4999-WY'    => 'Best for users who prefer full control and setup directly on the thermostat via an intuitive touchscreen display.',
    'UWG5-4999-WY'    => 'Best for users who want app-based control of their floor heating system, with the thermostat primarily used for basic temperature adjustments.',
    # ── Infrared Heating Panel & Towel Warmer Controls ──────────────────────────────────
    'GK16-30090-0006' => 'Simple timer, no programming',
    'GK16-30090-0002' => 'Scheduled daily heating',
    'GK16-30090-0003' => 'Smart home & voice control',
    # ── Snow Melting Controls ──────────────────────────────────────────────────
    'SC-MZ-TOUCH'     => 'Multi-zone touchscreen hub',
    'SCP-120'         => 'Basic single-zone manual',
    'SCA-DUAL'        => 'Automatic with external sensor',
    'SCE-120'         => 'Economical automatic control',
    'SCV-DUAL'        => 'Automatic, no sensor needed',
    'SCM-DUAL'        => 'Manual timer control',
    'SCW-120-15A'     => 'WiFi remote control',
    'SMRT-IQ'         => 'WiFi remote monitoring',
    # ── Pipe Freeze & Roof/Gutter Controls ────────────────────────────────────
    'AIR-STAT'        => 'Temperature-only freeze protection',
    'RXSJS2-Z1SP'     => 'Automatic freeze protection'
}.freeze
FEATURE_TOOLTIPS =

Feature tooltips.

{
  'automatic'                                    => 'Uses sensors to activate the system automatically when snow or ice is detected—no manual intervention needed.',
  'outdoor_rated'                                => 'The control enclosure (and sensors) are rated to withstand harsh outdoor environments including moisture, frost, and temperature extremes.',
  'moisture_sensor'                              => 'Detects the presence of snow, rain, or moisture above the heated surface. The system only activates when both moisture and a below-freezing air temperature are detected—preventing unnecessary heating.',
  'air_temp_sensor'                              => 'Monitors outdoor air temperature. Combined with moisture detection, it ensures the system only runs when conditions actually require it, saving energy.',
  'slab_temp_sensor'                             => 'Measures the surface temperature of the concrete or pavers directly. The system heats until the slab reaches the target temperature, then shuts off automatically.',
  'digital_temp_readout'                         => 'Shows the current sensor temperature on the control\'s display so you can monitor conditions without a separate thermometer.',
  'adjust_after_run_time'                        => 'Keeps the system running for a set time after precipitation stops to ensure the surface dries fully and prevent re-freezing.',
  'multi_zone_capability'                        => 'Controls multiple independent heating zones from a single unit—useful for driveways, walkways, and steps on the same installation.',
  'timer'                                        => 'A built-in 24-hour timer lets you schedule the system to run during specific hours. No sensors required—ideal for simple on/off control.',
  'countdown_timer'                              => 'Preset countdown durations let you run the system for a fixed period (e.g. 30 min, 1 hr, 2 hr, 4 hr) then shut off automatically—no programming required.',
  'control_remotely_via_iphone_android_or_web_app' => 'Connects via WiFi so you can monitor and control the system from anywhere using a smartphone app or web browser. Compatible with Alexa, Google Home, Nest, and IFTTT.',
  'max_load'                                     => 'The maximum load this control can switch directly.',
  'wifi'                                         => 'Built-in WiFi lets you monitor and adjust settings remotely from a smartphone app.',
  'remote_support'                               => 'Allows WarmlyYours technicians to diagnose issues and push firmware updates over the internet.',
  'voice_control'                                => 'Compatible with Amazon Alexa, Google Assistant, and Apple HomeKit/Siri for hands-free voice commands and home automation routines.',
  'programmable'                                 => 'Set daily on/off schedules so the system runs exactly when you need it without manual intervention.',
  'schedules'                                    => 'Number of independent on/off schedule events available for programming.',
  'floor_sensor_included'                        => 'Includes a floor temperature sensor so the thermostat can regulate the actual floor temperature, not just the air.',
  'included_sensor'                              => 'The temperature sensor shipped in the box with this control.',
  'temperature_setpoint'                         => 'The temperature range you can set for the control to maintain.',
  'built_in_gfi'                                 => 'Ground Fault Equipment Protection trips the circuit if a fault is detected, protecting both the equipment and personnel.',
  'waterproofing'                                => 'The NEMA enclosure rating indicates how well the control resists water, dust, and corrosion.',
  'indicators'                                   => 'Status lights or display that show the current operating state of the control.',
  'area_of_use'                                  => 'The environments where this control is certified for installation (e.g. non-hazardous indoor locations).',
  # Sensor-specific
  'sensor_installation'                          => 'Where and how this sensor is physically installed.',
  'detects_precipitation'                        => 'Senses snow, rain, or ice above the surface. Used with air temperature to decide when the system should activate.',
  'measures_air_temp'                            => 'Reads outdoor air temperature. Prevents the system from running when it\'s too warm to need heating.',
  'measures_slab_temp'                           => 'Reads the surface temperature of the concrete or pavers. The system runs until the slab reaches the set temperature, then shuts off.',
  'retrofit_compatible'                          => 'Can be installed after construction is complete—no need to break up or re-pour concrete.',
  'compatible_controls'                          => 'The WarmlyYours snow melting controls that accept this sensor type.'
}.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(skus: [], features: nil, section_title: nil, section_description: nil, sort_by_price: true, image_height: 180, image_overrides: {}) ⇒ ProductCompareTableComponent

Returns a new instance of ProductCompareTableComponent.



347
348
349
350
351
352
353
354
355
356
# File 'app/components/www/product_compare_table_component.rb', line 347

def initialize(skus: [], features: nil, section_title: nil, section_description: nil, sort_by_price: true, image_height: 180, image_overrides: {})
  @skus = Array(skus).compact
  @features = features
  @section_title = section_title
  @section_description = section_description
  @sort_by_price = sort_by_price
  @image_height = image_height
  @image_overrides = image_overrides
  super()
end

Instance Attribute Details

#featuresObject (readonly)

Returns the value of attribute features.



19
20
21
# File 'app/components/www/product_compare_table_component.rb', line 19

def features
  @features
end

#section_descriptionObject (readonly)

Returns the value of attribute section_description.



19
20
21
# File 'app/components/www/product_compare_table_component.rb', line 19

def section_description
  @section_description
end

#section_titleObject (readonly)

Returns the value of attribute section_title.



19
20
21
# File 'app/components/www/product_compare_table_component.rb', line 19

def section_title
  @section_title
end

#skusObject (readonly)

Returns the value of attribute skus.



19
20
21
# File 'app/components/www/product_compare_table_component.rb', line 19

def skus
  @skus
end

Instance Method Details

#add_on_for(value) ⇒ Object

When a feature value is a Symbol, returns the matching add-on product hash
{ sku:, label: } so the template can render a product link.
Returns nil for non-Symbol values.



382
383
384
385
386
# File 'app/components/www/product_compare_table_component.rb', line 382

def add_on_for(value)
  return nil unless value.is_a?(Symbol)

  ADD_ON_PRODUCTS[value]
end

#add_on_url(add_on) ⇒ Object

Returns the URL for an add-on product.
Prefers an on-page anchor (e.g. "#sensors") when set, otherwise falls back
to the product detail URL.



391
392
393
394
395
# File 'app/components/www/product_compare_table_component.rb', line 391

def add_on_url(add_on)
  return add_on[:anchor] if add_on[:anchor].present?

  helpers.catalog_link_for_sku(add_on[:sku])
end

#before_renderObject



358
359
360
361
# File 'app/components/www/product_compare_table_component.rb', line 358

def before_render
  @products = load_products
  @features = detect_features if @features.blank?
end

#feature_display_name(feature_key) ⇒ Object



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'app/components/www/product_compare_table_component.rb', line 397

def feature_display_name(feature_key)
  case feature_key
  when 'automatic'
    'Automatic'
  when 'outdoor_rated'
    'Outdoor Rated'
  when 'moisture_sensor'
    'Moisture Sensor'
  when 'air_temp_sensor'
    'Air Temp Sensor'
  when 'slab_temp_sensor'
    'Slab Temp Sensor'
  when 'digital_temp_readout'
    'Digital Temp Readout'
  when 'adjust_after_run_time'
    'Adjust After Run Time'
  when 'multi_zone_capability'
    'Multi Zone Capability'
  when 'timer'
    'Timer'
  when 'countdown_timer'
    'Countdown Timer'
  when 'control_remotely_via_iphone_android_or_web_app'
    'Control Remotely Via iPhone/Android/Web App'
  when 'max_load'
    'Max Load'
  # Thermostat-specific features
  when 'wifi'
    'WiFi-Enabled'
  when 'remote_support'
    'Remote Troubleshooting/Updates'
  when 'voice_control'
    'Voice Assistant Compatible'
  when 'programmable'
    'Programmable'
  when 'schedules'
    'Schedules'
  when 'floor_sensor_included'
    'Floor Sensor Included'
  when 'display'
    'Display'
  # Pipe Freeze Protection specific features
  when 'included_sensor'
    'Included Sensor'
  when 'temperature_setpoint'
    'Temperature Setpoint'
  when 'indicators'
    'Status Indicators'
  when 'built_in_gfi'
    'Built-in GFI Protection'
  when 'waterproofing'
    'Enclosure Rating'
  when 'area_of_use'
    'Area of Use'
  # Sensor-specific features
  when 'sensor_installation'
    'Installation'
  when 'detects_precipitation'
    'Detects Precipitation'
  when 'measures_air_temp'
    'Measures Air Temperature'
  when 'measures_slab_temp'
    'Measures Slab Temperature'
  when 'retrofit_compatible'
    'Retrofit Compatible'
  when 'compatible_controls'
    'Compatible Controls'
  else
    feature_key.to_s.humanize
  end
end

#feature_tooltip(feature_key) ⇒ Object

Returns a short plain-text tooltip description for the given feature, or nil if none defined.



371
372
373
# File 'app/components/www/product_compare_table_component.rb', line 371

def feature_tooltip(feature_key)
  FEATURE_TOOLTIPS[feature_key]
end

#feature_value(product, feature_key) ⇒ Object



363
364
365
366
367
368
# File 'app/components/www/product_compare_table_component.rb', line 363

def feature_value(product, feature_key)
  sku = product[:sku]

  # Use hash table lookup instead of nested case statements
  SKU_FEATURES.dig(sku, feature_key) || false
end

#sensor_feature?(feature_key) ⇒ Boolean

Returns:

  • (Boolean)


375
376
377
# File 'app/components/www/product_compare_table_component.rb', line 375

def sensor_feature?(feature_key)
  SENSOR_FEATURE_KEYS.include?(feature_key)
end