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.



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'app/helpers/pages_helper.rb', line 271

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



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'app/helpers/pages_helper.rb', line 194

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



141
142
143
# File 'app/helpers/pages_helper.rb', line 141

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



188
189
190
191
192
# File 'app/helpers/pages_helper.rb', line 188

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 = {}) ⇒ ActiveSupport::SafeBuffer

New: H1 header helper matching our H2/H3 helpers

Parameters:

  • title (String)

    header text

  • options (Hash) (defaults to: {})

    HTML attributes for the <h1>

Options Hash (options):

  • :class (String)

    CSS class(es) (default
    'text-red fw-light display-4')

  • :extra_class (String)

    extra CSS class(es) appended to
    :class (and forwarded as an attribute)

  • :data (Hash)

    data attributes; Swiftype title metadata is
    merged in under :swiftype

Returns:

  • (ActiveSupport::SafeBuffer)

    the <h1> tag



43
44
45
46
47
48
49
50
51
52
# File 'app/helpers/pages_helper.rb', line 43

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 = {}) ⇒ ActiveSupport::SafeBuffer

H2 page header with default styling; extra keys become <h2> attributes.

Parameters:

  • title (String)

    header text

  • options (Hash) (defaults to: {})

    HTML attributes for the <h2>

Options Hash (options):

  • :class (String)

    CSS class(es) (default 'text-red fw-light')

  • :extra_class (String)

    extra CSS class(es) appended to
    :class (and forwarded as an attribute)

Returns:

  • (ActiveSupport::SafeBuffer)

    the <h2> tag



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

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 = {}) ⇒ ActiveSupport::SafeBuffer

H3 page header with default styling; extra keys become <h3> attributes.

Parameters:

  • title (String)

    header text

  • options (Hash) (defaults to: {})

    HTML attributes for the <h3>

Options Hash (options):

  • :class (String)

    CSS class(es) (default 'text-red fw-light')

  • :extra_class (String)

    extra CSS class(es) appended to
    :class (and forwarded as an attribute)

Returns:

  • (ActiveSupport::SafeBuffer)

    the <h3> tag



27
28
29
30
31
# File 'app/helpers/pages_helper.rb', line 27

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)

  • source (Object, nil) (defaults to: nil)
  • 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)

Returns:

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


113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'app/helpers/pages_helper.rb', line 113

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:



179
180
181
# File 'app/helpers/pages_helper.rb', line 179

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:



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'app/helpers/pages_helper.rb', line 79

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:



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

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.



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'app/helpers/pages_helper.rb', line 230

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.



214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'app/helpers/pages_helper.rb', line 214

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