Module: PagesHelper

Includes:
UrlsHelper
Included in:
Www::VideoSectionComponent
Defined in:
app/helpers/pages_helper.rb

Overview

View helper: pages.

Instance Method Summary collapse

Methods included from UrlsHelper

#catalog_breadcrumb_links, #catalog_link, #catalog_link_for_product_line, #catalog_link_for_sku, #cms_link, #delocalized_path, #path_to_sales_product_sku, #path_to_sales_product_sku_for_product_line, #path_to_sales_product_sku_for_product_line_slug, #product_line_from_catalog_link, #protocol_neutral_url, #sanitize_external_url, #valid_external_url?

Instance Method Details

Standard trust-badge strip for a landing-page hero banner.

Returns the array Www::FullWidthLandingPageHeaderComponent expects in
its banner_badges: argument. It wraps a shared core — review rating,
"Radiant Experts Since 1999", "UL/cUL Listed", "24/7 Support" and
"Same Day Shipping" — so a page opts in with one line instead of
hand-rolling the array (which let the popover wording drift page to
page).

Parameters:

  • variant (Symbol) (defaults to: :floor_heating)

    :floor_heating (default) or :snow_melting.
    :floor_heating defaults the page-specific (4th) slot to
    "Easy DIY Install". :snow_melting defaults it to "30+ Years
    Lifespan" and appends a "No Hassle Returns" badge — snow-melting
    systems are professionally installed, so no DIY claim is made.

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

    page-specific badge(s) for the
    4th slot, replacing the variant default (e.g. environ-mats'
    "No Mortar Required").

  • review (Boolean) (defaults to: true)

    include the dynamic review-rating badge. The
    badge links to #reviews; pass false on pages with no reviews
    section so the link does not dangle.

Returns:

  • (Array<Hash>)

    badge hashes, compacted — the review badge is
    nil when ReviewsIo company stats are unavailable.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'app/helpers/pages_helper.rb', line 248

def banner_trust_badges(variant: :floor_heating, extra: nil, review: true)
  fourth_slot = Array.wrap(extra).presence || [default_banner_badge(variant)]

  badges = [
    (review_trust_badge if review),
    banner_badge('calendar-check', 'Radiant Experts Since 1999'),
    banner_badge('certificate', 'UL/cUL Listed',
                 popover_title: 'UL and cUL Listed',
                 popover_content: 'Our heating systems are independently tested and certified ' \
                                  'for safety and performance, meeting rigorous UL and cUL ' \
                                  'standards for North American homes.'),
    *fourth_slot,
    banner_badge('headset', '24/7 Support',
                 popover_title: '24/7 Installation Support',
                 popover_content: 'Our US-based technical experts are available 24/7 to guide ' \
                                  'you through every step of your installation.'),
    banner_badge('truck-fast', 'Same Day Shipping',
                 popover_title: 'Same Day Shipping',
                 popover_content: 'With 99% of orders shipping the same day, your order arrives ' \
                                  'quickly so your project stays on track.')
  ]

  if variant == :snow_melting
    badges << banner_badge('rotate-left', 'No Hassle Returns',
                           popover_title: 'No Hassle Returns',
                           popover_content: 'Return unused products anytime with no restocking ' \
                                            'fees, making your purchase completely risk-free.')
  end

  badges.compact
end

#heated_driveway_cost_rowsObject



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'app/helpers/pages_helper.rb', line 171

def heated_driveway_cost_rows
  if canada?
    { range: 'C$3,200 and C$25,000', event_range: 'C$3–C$18', controls: 'C$799–C$5,299',
      sizes: [
        { name: 'Small (10ʹ × 20ʹ)', full_sqft: 200, tt_sqft: 80, full_cost: '~C$3,200', tt_cost: '~C$2,400' },
        { name: 'Standard (20ʹ × 20ʹ)', full_sqft: 400, tt_sqft: 160, full_cost: '~C$6,200', tt_cost: '~C$3,950' },
        { name: 'Large (30ʹ × 20ʹ)', full_sqft: 600, tt_sqft: 240, full_cost: '~C$9,000+', tt_cost: '~C$4,500+' }
      ] }
  else
    { range: '$2,500 and $20,000', event_range: '$3–$20', controls: '$629–$3,799',
      sizes: [
        { name: 'Small (10ʹ × 20ʹ)', full_sqft: 200, tt_sqft: 80, full_cost: '~$2,500', tt_cost: '~$1,850' },
        { name: 'Standard (20ʹ × 20ʹ)', full_sqft: 400, tt_sqft: 160, full_cost: '~$4,750', tt_cost: '~$2,360' },
        { name: 'Large (30ʹ × 20ʹ)', full_sqft: 600, tt_sqft: 240, full_cost: '~$7,000+', tt_cost: '~$3,500+' }
      ] }
  end
end

#page_article_faqs(tag: main_tag) ⇒ Array<Article>

Retrieve FAQs tagged with the given tag, including vote data.

Parameters:

  • tag (String) (defaults to: main_tag)

    The tag to filter by (default: main_tag)

Returns:

  • (Array<Article>)

    Array of FAQ articles with vote data



118
119
120
# File 'app/helpers/pages_helper.rb', line 118

def page_article_faqs(tag: main_tag)
  retrieve_faqs(tags: tag, add_vote_data: true)
end

#page_banner_image(tag: main_tag&.sub(/\Afor-/, 'banner-for-')) ⇒ Image?

Look up the most recent Image tagged with the banner tag for this page.
Derives the banner tag from main_tag by swapping the "for-" prefix to "banner-for-".

Parameters:

  • tag (String, nil) (defaults to: main_tag&.sub(/\Afor-/, 'banner-for-'))

    Override banner tag (default: derived from main_tag)

Returns:

  • (Image, nil)

    The most recent matching image, or nil



165
166
167
168
169
# File 'app/helpers/pages_helper.rb', line 165

def page_banner_image(tag: main_tag&.sub(/\Afor-/, 'banner-for-'))
  return nil if tag.blank?

  Image.tagged_with(tag).order(created_at: :desc).first
end

#page_header_h1(title, options = {}) ⇒ Object

New: H1 header helper matching our H2/H3 helpers



20
21
22
23
24
25
26
27
28
29
# File 'app/helpers/pages_helper.rb', line 20

def page_header_h1(title, options = {})
  # Default H1 styling to match design system (large, light weight, primary color)
  options[:class] ||= 'text-red fw-light display-4'
  options[:class] = "#{options[:class]} #{options[:extra_class]}" if options[:extra_class].present?
  options[:data] ||= {}
  options[:data][:swiftype] ||= {}
  options[:data][:swiftype][:name] = 'title'
  options[:data][:swiftype][:type] = 'string'
  tag.h1 title, **options
end

#page_header_h2(title, options = {}) ⇒ Object



7
8
9
10
11
# File 'app/helpers/pages_helper.rb', line 7

def page_header_h2(title, options = {})
  options[:class] ||= 'text-red fw-light'
  options[:class] = "#{options[:class]} #{options[:extra_class]}" if options[:extra_class].present?
  tag.h2 title, **options
end

#page_header_h3(title, options = {}) ⇒ Object



13
14
15
16
17
# File 'app/helpers/pages_helper.rb', line 13

def page_header_h3(title, options = {})
  options[:class] ||= 'text-red fw-light'
  options[:class] = "#{options[:class]} #{options[:extra_class]}" if options[:extra_class].present?
  tag.h3 title, **options
end

#page_posts(tag: main_tag, source: nil, limit: 12, backfill_tag: nil, min: nil) ⇒ ActiveRecord::Relation<Post>, Array<Post>

Retrieve published posts tagged with the given tag, ordered by publication date.

When +backfill_tag+ is provided, materialises the primary results and tops up
from the backfill tag when the count falls below +min+ (defaults to +limit+).
Returns an Array in that case; otherwise returns an ActiveRecord relation.

Parameters:

  • tag (String) (defaults to: main_tag)

    The tag to filter by (default: main_tag)

  • limit (Integer) (defaults to: 12)

    Maximum number of posts to return (default: 12)

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

    Broader tag to fill gaps from

  • min (Integer, nil) (defaults to: nil)

    Minimum count before backfill kicks in (default: limit)

  • source (Object, nil) (defaults to: nil)

Returns:

  • (ActiveRecord::Relation<Post>, Array<Post>)


90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'app/helpers/pages_helper.rb', line 90

def page_posts(tag: main_tag, source: nil, limit: 12, backfill_tag: nil, min: nil)
  if source.respond_to?(:linked_posts)
    linked = source.linked_posts
    return linked if linked.any?
  end

  primary = Post.published
                .tagged_with(tag)
                .includes(:preview_image, original_author: :profile_image)
                .order(published_at: :desc)
                .limit(limit)

  return primary unless backfill_tag

  results = primary.to_a
  threshold = min || limit
  return results if results.size >= threshold

  backfill = page_posts(tag: backfill_tag, limit: limit - results.size)
             .excluding(results)
             .to_a
  results + backfill
end

#page_publications(tag: main_tag) ⇒ Array<Publication>

Retrieve publications (documents) tagged with the given tag in the current locale.

Parameters:

  • tag (String) (defaults to: main_tag)

    The tag to filter by (default: main_tag)

Returns:



156
157
158
# File 'app/helpers/pages_helper.rb', line 156

def page_publications(tag: main_tag)
  find_publications_by_tag_in_current_locale(tag).to_a
end

#page_showcases(tag: main_tag, limit: 20, backfill_tag: nil, min: nil) ⇒ ActiveRecord::Relation<Showcase>, Array<Showcase>

Retrieve showcases tagged with the given tag, ordered by image count and recency.

When +backfill_tag+ is provided, materialises the primary results and tops up
from the backfill tag when the count falls below +min+ (defaults to +limit+).
Returns an Array in that case; otherwise returns an ActiveRecord relation.

Parameters:

  • tag (String) (defaults to: main_tag)

    The tag to filter by (default: main_tag)

  • limit (Integer) (defaults to: 20)

    Maximum number of showcases to return (default: 20)

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

    Broader tag to fill gaps from

  • min (Integer, nil) (defaults to: nil)

    Minimum count before backfill kicks in (default: limit)

Returns:



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/helpers/pages_helper.rb', line 56

def page_showcases(tag: main_tag, limit: 20, backfill_tag: nil, min: nil)
  primary = Showcase.published
                    .tags_include(tag)
                    .includes(:region_state, showcase_digital_assets: :digital_asset)
                    .left_joins(:showcase_digital_assets)
                    .select('showcases.*, COUNT(showcase_digital_assets.id) AS images_count')
                    .group('showcases.id')
                    .order('images_count DESC, showcases.updated_at DESC')
                    .limit(limit)

  return primary unless backfill_tag

  results = primary.to_a
  threshold = min || limit
  return results if results.size >= threshold

  backfill = page_showcases(tag: backfill_tag, limit: limit - results.size)
             .excluding(results)
             .to_a
  results + backfill
end

#page_videos(tag: main_tag, limit: 20, backfill_tag: nil, min: nil) ⇒ ActiveRecord::Relation<Video>, Array<Video>

Retrieve public videos tagged with the given tag, ordered by creation date.

When +backfill_tag+ is provided, materialises the primary results and tops up
from the backfill tag when the count falls below +min+ (defaults to +limit+).
Returns an Array in that case; otherwise returns an ActiveRecord relation.

Parameters:

  • tag (String) (defaults to: main_tag)

    The tag to filter by (default: main_tag)

  • limit (Integer) (defaults to: 20)

    Maximum number of videos to return (default: 20)

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

    Broader tag to fill gaps from

  • min (Integer, nil) (defaults to: nil)

    Minimum count before backfill kicks in (default: limit)

Returns:



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'app/helpers/pages_helper.rb', line 133

def page_videos(tag: main_tag, limit: 20, backfill_tag: nil, min: nil)
  primary = Video.public_videos
                 .tagged_with(tag)
                 .includes(:poster_image)
                 .order(created_at: :desc)
                 .limit(limit)

  return primary unless backfill_tag

  results = primary.to_a
  threshold = min || limit
  return results if results.size >= threshold

  backfill = page_videos(tag: backfill_tag, limit: limit - results.size)
             .excluding(results)
             .to_a
  results + backfill
end

#review_benefit_cardObject

Dynamic benefit card for BenefitsListComponent (homepage "Why Choose Us").
Uses the same cached company-wide stats as review_trust_badge.



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'app/helpers/pages_helper.rb', line 207

def review_benefit_card
  stats = ReviewsIo.company_wide_stats
  return nil if stats[:satisfaction_pct].blank?

  count_label = if stats[:num] >= 1000
                  "#{(stats[:num] / 100) * 100}+"
                else
                  stats[:num].to_s
                end

  {
    # Localized path; BenefitsListComponent also wraps with cms_link (idempotent for /paths).
    url: cms_link('/reviews'),
    icon: 'trophy-star',
    title: 'Unparalleled Customer Satisfaction',
    description: "#{stats[:satisfaction_pct]}% satisfaction from #{count_label} reviews"
  }
end

#review_trust_badgeObject

Dynamic trust badge for company-wide review stats (cached 7 days).
Returns a hash compatible with banner_badges in FullWidthLandingPageHeaderComponent.



191
192
193
194
195
196
197
198
199
200
201
202
203
# File 'app/helpers/pages_helper.rb', line 191

def review_trust_badge
  stats = ReviewsIo.company_wide_stats
  return nil unless stats[:star_avg].present? && stats[:num].positive?

  count_label = if stats[:num] >= 1000
                  "#{(stats[:num] / 100) * 100}+ Reviews"
                else
                  "#{stats[:num]} Reviews"
                end

  { icon: 'star', icon_family: 'fas', icon_class: 'text-warning',
    label: "#{stats[:star_avg]}/5", subtitle: count_label, url: '#reviews' }
end