Module: Www::NavbarHelper
- Included in:
- DesktopMenuComponent, MobileMenuComponent, NavbarMenuComponent
- Defined in:
- app/helpers/www/navbar_helper.rb
Overview
View helper: navbar.
Constant Summary collapse
- NAVBAR_MENUS =
Centralized navigation menu structure
Used by both desktop mega-menus and mobile accordion menu
Update this single source to keep both menus in syncItem options:
label: (required) Display text
path: (required) URL path
icon: (optional) Font Awesome icon name (e.g., 'phone', 'envelope')
badge: (optional) Badge text (e.g., 'New', 'Sale')
badge_class: (optional) Badge CSS class (default: 'bg-success')
image: (optional) Image URL for visual items (like towel warmer finishes)
image_alt: (optional) Alt text for image
offcanvas: (optional) If true, opens an offcanvas instead of navigating
desktop_only: (optional) If true, only shows on desktop
mobile_only: (optional) If true, only shows on mobileCategory options:
header_link: (optional) Makes the category header a clickable link (e.g., '/trade')Menu-specific quick_links: (optional) Each product menu can have its own Quick Links column
quick_links: { header: 'Quick Links', items: [...] } { floor_heating: { label: 'Floor Heating', quick_links: { header: 'Quick Links', items: [ { label: 'On Sale', path: '/sales', icon: 'tags', highlight: true }, { label: 'Refurbished', path: '/sales/refurbished', icon: 'recycle' }, { label: 'Find Your Quote', path: '/quote-lookup', icon: 'file-invoice' }, { label: 'Get Samples', path: '/contact/product-sample', icon: 'swatchbook' }, { label: 'Support', path: '/support', icon: 'headset' }, { label: 'Training', path: '/product-training#floor-heating-training', icon: 'graduation-cap' }, { label: 'Warranty', path: '/contact/warranty-floor-heating', icon: 'shield-check' }, { label: 'Download Catalog', icon: 'download', publication_skus: %w[WARMLYYOURS-ELECTRIC-FLOOR-HEATING-CATALOG-USA WARMLYYOURS-ELECTRIC-FLOOR-HEATING-CATALOG-CANADA] } ] }, categories: [ { header: 'Floor Heating Systems', header_link: '/floor-heating', card_layout: true, items: [ { label: 'TempZone™ Heating Mat', path: '/floor-heating/heated-floor-mat', image: 'https://ik.warmlyyours.com/img/tempzone-flex-with-backup-sensor-hifjrk?tr=w-240,h-240,fo-auto', description: 'Easiest install for tile & stone' }, { label: 'TempZone™ Heating Cable', path: '/floor-heating/heating-cable', image: 'https://ik.warmlyyours.com/img/tz-cable-with-sensor-2muand?tr=w-240,h-240,fo-auto', description: 'Flexible & cost-effective for any room' }, { label: 'TempZone™ Custom Mats', path: '/floor-heating/custom-mats', image: 'https://ik.warmlyyours.com/img/tempzone-custom-mat-with-sensor-erzuv5?tr=w-240,h-240,fo-auto', description: 'Made-to-order for any room shape' }, { label: 'Environ™ Heating Mat', path: '/floor-heating/environ-mats', image: 'https://ik.warmlyyours.com/img/environ-flex-with-backup-sensor-y3z2rh?tr=w-240,h-240,fo-auto', description: 'Designed for floating floors' }, { label: 'Slab Heating Systems', path: '/floor-heating/slab-heating', image: 'https://ik.warmlyyours.com/img/slab-roll-with-sensor-muwu4e?tr=w-240,h-240,fo-auto', description: 'Embedded power for concrete slabs' }, { label: 'Thermostats', path: '/floor-heating/thermostats', image: 'https://ik.warmlyyours.com/img/nspire-touch-thermostat-angle-with-82-image-b3t9oe?tr=w-240,h-240,fo-auto', description: 'Smart controls for your heat' }, { label: 'Underlayments', path: '/floor-heating/underlayment', image: 'https://ik.warmlyyours.com/img/prodeso-roll-b7d050.png?tr=w-240,h-240,fo-auto', description: 'Sound & thermal insulation' }, { label: 'Accessories', path: '/floor-heating/accessories', image: 'https://ik.warmlyyours.com/img/replacement-or-backup-floor-sensor-sensor2-1f8443.png?tr=w-240,h-240,fo-auto', description: 'Sensors, cables & add-ons' } ] }, { # Stacked sections: Tools header + items, then Resources header + items sections: [ { header: 'Tools', items: [ { label: 'Design Your Room', path: '/tools/online-design-tool', icon: 'pen-ruler' }, { label: 'Get an Instant Quote', path: '/floor-heating/quote-builder', icon: 'calculator' }, { label: 'Estimate Running Costs', path: '/floor-heating/cost-calculator', icon: 'chart-line' }, { label: 'Send us your Plan', path: '/floor-heating/smartplan', icon: 'paper-plane' }, { label: 'Heat Loss Calculator', path: '/floor-heating/heatloss-calculator', icon: 'temperature-arrow-down' } ] }, { header: 'Resources', items: [ { label: 'Floor Heating Videos', path: '/floor-heating/videos', icon: 'circle-play' }, { label: 'Floor Heating Plan Finder', path: '/floor-heating-floor-plans', icon: 'map', description: 'Select the plan that matches your current or desired remodel to instantly ' \ 'explore tailored product quotes, costs, and installation plans.' }, { label: 'Project Showcase', path: '/floor-heating/showcases', icon: 'images' }, { label: 'Pets/Animals Guide', path: '/floor-heating/pets', icon: 'paw' }, { label: 'Radiant Heating Guide', path: '/radiant-heating', icon: 'book' } ] } ] }, { header: 'Explore by Room', items: [ { label: 'Bathroom', path: '/floor-heating/bathroom' }, { label: 'Shower', path: '/floor-heating/shower' }, { label: 'Kitchen', path: '/floor-heating/kitchen' }, { label: 'Living Room', path: '/floor-heating/living-room' }, { label: 'Bedroom', path: '/floor-heating/bedroom' }, { label: 'Basement', path: '/floor-heating/basement' }, { label: 'Laundry & Mudroom', path: '/floor-heating/laundry-mudroom' }, { label: 'Sunroom', path: '/floor-heating/sunroom' }, { label: 'Tiny Houses', path: '/floor-heating/tiny-house' } ] }, { header: 'Explore by Flooring', items: [ { label: 'Tile, Marble, Stone', path: '/floor-heating/tile-marble-or-stone' }, { label: 'Luxury Vinyl', path: '/floor-heating/luxury-vinyl-tiles' }, { label: 'Laminate', path: '/floor-heating/laminate' }, { label: 'Engineered Wood', path: '/floor-heating/engineered-wood' }, { label: 'Nailed Hardwood', path: '/floor-heating/nailed-hardwood' }, { label: 'Bamboo', path: '/floor-heating/bamboo' }, { label: 'Carpet', path: '/floor-heating/carpet' }, { label: 'Concrete', path: '/floor-heating/concrete' } ] } ] }, snow_melting: { label: 'Snow Melting', quick_links: { header: 'Quick Links', items: [ { label: 'On Sale', path: '/sales', icon: 'tags', highlight: true }, { label: 'Find Your Quote', path: '/quote-lookup', icon: 'file-invoice' }, { label: 'Get Samples', path: '/contact/product-sample', icon: 'swatchbook' }, { label: 'Support', path: '/support', icon: 'headset' }, { label: 'Training', path: '/product-training#snow-melting-training', icon: 'graduation-cap' }, { label: 'Warranty', path: '/contact/warranty-snow-melting', icon: 'shield-check' }, { label: 'Download Catalog', icon: 'download', publication_skus: %w[WARMLYYOURS-OUTDOOR-HEATING-CATALOG-USA WARMLYYOURS-OUTDOOR-HEATING-CATALOG-CANADA] } ] }, categories: [ { header: 'Snow Melting Systems', header_link: '/snow-melting', card_layout: true, items: [ { label: 'PowerMat', path: '/snow-melting/mat/powermat', image: 'https://ik.warmlyyours.com/img/iq-snow-melting-mat-thumbnail-a0e07c.png?tr=w-240,h-240,fo-auto', description: 'Maximum heat for heavy snowfall' }, { label: 'OmniMat', path: '/snow-melting/mat/omnimat', image: 'https://ik.warmlyyours.com/img/iq-snow-melting-mat-thumbnail-a0e07c.png?tr=w-240,h-240,fo-auto', description: 'Balanced performance & efficiency' }, { label: 'Cable', path: '/snow-melting/cable', image: 'https://ik.warmlyyours.com/img/iq-snow-melt-cable-thumbnail-2ff059.png?tr=w-240,h-240,cm-pad_resize,bg-FFFFFF', description: 'Flexible for any area' }, { label: 'Controls & Relays', path: '/snow-melting/controls', image: 'https://ik.warmlyyours.com/img/sce-120-slab-ss-dmglsy.jpeg?tr=w-240,h-240,cm-pad_resize,bg-FFFFFF', description: 'Sensors & Power Controllers' }, { label: 'Accessories', path: '/snow-melting/accessories', image: 'https://ik.warmlyyours.com/img/snow-melt-plaque-nec-requirement-smp-bf0b13.png?tr=w-240,h-240,cm-pad_resize,bg-FFFFFF', description: 'Installation aides & Splices' } ] }, { sections: [ { header: 'Tools', items: [ { label: 'Get an Instant Quote', path: '/snow-melting/quote-builder', icon: 'calculator' }, { label: 'Running Cost Estimator', path: '/snow-melting/cost-calculator', icon: 'chart-line' }, { label: 'Request Installation Plan', path: '/snow-melting/smartplan', icon: 'paper-plane' } ] }, { header: 'Resources', items: [ { label: 'Snow Melting Videos', path: '/snow-melting/videos', icon: 'circle-play' }, { label: 'Snow Melt Plan Finder', path: '/snow-melting-plans', icon: 'map', description: 'Select the plan that matches your outdoor layout or upcoming project to ' \ 'instantly explore tailored product quotes, costs, and installation plans.' }, { label: 'Project Showcase', path: '/snow-melting/showcases', icon: 'images' } ] } ] }, { header: 'Explore by Application', items: [ { label: 'Driveways', path: '/snow-melting/heated-driveway' }, { label: 'Walkways & Paths', path: '/snow-melting/heated-walkway-path' }, { label: 'Outdoor Stairs', path: '/snow-melting/outdoor-stairs' }, { label: 'Ramps', path: '/snow-melting/accessibility-ramp-deicing' }, { label: 'Patios', path: '/snow-melting/heated-concrete-patio' } ] }, { header: 'Explore by Surface', items: [ { label: 'Asphalt', path: '/snow-melting/asphalt' }, { label: 'Concrete', path: '/snow-melting/concrete' }, { label: 'Pavers/Stone', path: '/snow-melting/heated-pavers-and-stone' } ] } ] }, deicing: { label: 'Deicing', quick_links: { header: 'Quick Links', items: [ { label: 'On Sale', path: '/sales', icon: 'tags', highlight: true }, { label: 'Videos', path: '/videos', icon: 'circle-play' }, { label: 'Find Your Quote', path: '/quote-lookup', icon: 'file-invoice' }, { label: 'Support', path: '/support', icon: 'headset' }, { label: 'Training', path: '/product-training#roof-gutter-deicing-training', icon: 'graduation-cap' }, { label: 'Warranty', path: '/warranty-registration', icon: 'shield-check' }, { label: 'Download Catalog', icon: 'download', publication_skus: %w[WARMLYYOURS-OUTDOOR-HEATING-CATALOG-USA WARMLYYOURS-OUTDOOR-HEATING-CATALOG-CANADA] } ] }, categories: [ { header: 'Pipe Freeze & Heat Trace', header_link: '/pipe-freeze-protection', card_layout: true, items: [ { label: 'Heat Tape Systems', path: '/pipe-freeze-protection', image: 'https://ik.warmlyyours.com/img/pipe-pro-tect-trace-heating-plug-in-cable-4115a8.png?tr=w-240,h-240,fo-auto', description: 'Pipe freeze protection' }, { label: 'Controls', path: '/pipe-freeze-protection/controls', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/premium-snow-melting-control-577303.png', description: 'Sensors & thermostats' } ] }, { header: 'Roof & Gutter Deicing', header_link: '/roof-and-gutter-deicing', card_layout: true, items: [ { label: 'Roof & Gutter Systems', path: '/roof-and-gutter-deicing', image: 'https://ik.warmlyyours.com/img/roof-and-gutter-kit-with-clips-xcgdu9?tr=w-240,h-240,fo-auto', description: 'Ice dam prevention' }, { label: 'Controls', path: '/roof-and-gutter-deicing/controls', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/premium-snow-melting-control-577303.png', description: 'Sensors & thermostats' }, { label: 'Accessories', path: '/roof-and-gutter-deicing/accessories', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/roof-and-gutter-de-icing-cable-19f875.png', description: 'Clips, kits & parts' } ] }, { header: 'By Roof Type', grouped_with_previous: true, items: [ { label: 'Metal Roofs', path: '/roof-and-gutter-deicing/metal-roof' }, { label: 'Asphalt Shingle Roofs', path: '/roof-and-gutter-deicing/asphalt-shingle-roof' }, { label: 'Slate Shingle Roofs', path: '/roof-and-gutter-deicing/slate-shingle-roof' } ] } ] }, towel_warmers: { label: 'Towel Warmers', quick_links: { header: 'Quick Links', items: [ { label: 'On Sale', path: '/sales', icon: 'tags', highlight: true }, { label: 'Towel Warmer Videos', path: '/towel-warmer/videos', icon: 'circle-play' }, { label: 'Find Your Quote', path: '/quote-lookup', icon: 'file-invoice' }, { label: 'Support', path: '/support', icon: 'headset' }, { label: 'Training', path: '/product-training#other-products-training', icon: 'graduation-cap' }, { label: 'Warranty', path: '/contact/warranty-towel-warmer', icon: 'shield-check' }, { label: 'Download Catalog', icon: 'download', publication_skus: %w[WARMLYYOURS-TOWEL-WARMER-CATALOG-USA WARMLYYOURS-TOWEL-WARMER-CATALOG-CANADA] } ] }, categories: [ { header: 'Towel Warmers', header_link: '/towel-warmer', card_layout: true, items: [ { label: 'Wall-Mounted', path: '/towel-warmer/wall-mounted', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/sierra-gold-t8k4hg', description: 'Hardwired & plug-in' }, { label: 'Freestanding', path: '/towel-warmer/freestanding', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/aaa-barcelona-niqcla', description: 'Portable & flexible' }, { label: 'Controls', path: '/towel-warmer/controls', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/timer-wifi-black-1mmzvp.jpeg', description: 'Timers & WiFi switches' }, { label: 'Crystal Accents', path: '/towel-warmer/crystal', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/wifi-front-platinum-wf4snx.png', description: 'Faceplates & collar accents' } ] }, { header: 'Shop by Finish', card_layout: true, items: [ { label: 'Brushed Stainless', path: '/towel-warmer/brushed-stainless-steel', image: 'https://ik.warmlyyours.com/tr:cm-extract,x-517,y-88,w-300,h-300,tr:w-240,h-240/img/riviera-brushed-towel-warmer-detail-0159da.jpeg', description: 'Fingerprint-resistant classic' }, { label: 'Polished Stainless', path: '/towel-warmer/polished-stainless-steel', image: 'https://ik.warmlyyours.com/tr:cm-extract,x-531,y-82,w-260,h-260,tr:w-240,h-240/img/riviera-towel-warmer-polished-detail-4d40bc.jpeg', description: 'Chrome-like mirror finish' }, { label: 'Matte Black', path: '/towel-warmer/matte-black', image: 'https://ik.warmlyyours.com/tr:cm-extract,x-2460,y-1280,w-382,h-382,tr:w-240,h-240/img/tws2-tah07kh-tahoe-7-black-3ebda2.png', description: 'Bold modern statement' }, { label: 'Gold', path: '/towel-warmer/gold', image: 'https://ik.warmlyyours.com/tr:w-240,h-240/img/tws2-tah07gh-tahoe-7-brushed-gold-swatch-87lc7b.png', description: 'Brushed & polished gold' } ] }, { # TODO: revisit visual treatment once we have product image crops that # actually represent size (current product photos didn't read as # small/medium/large). header: 'Shop by Size', items: [ { label: 'Compact', path: '/towel-warmer/compact', icon: 'compress', subtitle: 'Under 30″ · 4–6 bars' }, { label: 'Standard', path: '/towel-warmer/standard', icon: 'bars', subtitle: '30–40″ · 8–9 bars' }, { label: 'Large', path: '/towel-warmer/large', icon: 'expand', subtitle: 'Over 40″ · 10+ bars' } ] }, { # TODO: swap icons for bar-profile crops once source images are picked. header: 'Shop by Bar Shape', items: [ { label: 'Round', path: '/towel-warmer/round-bars', icon: 'circle', subtitle: 'Classic curved bars' }, { label: 'Square', path: '/towel-warmer/square-bars', icon: 'square', subtitle: 'Modern geometric' }, { label: 'Flat', path: '/towel-warmer/flat-bars', icon: 'grip-lines', subtitle: 'Wide drying surface' } ] } ] }, comfort_products: { label: 'Comfort Products', quick_links: { header: 'Quick Links', items: [ { label: 'On Sale', path: '/sales', icon: 'tags', highlight: true }, { label: 'Videos', path: '/videos', icon: 'circle-play' }, { label: 'Find Your Quote', path: '/quote-lookup', icon: 'file-invoice' }, { label: 'Support', path: '/support', icon: 'headset' }, { label: 'Training', path: '/product-training#other-products-training', icon: 'graduation-cap' }, { label: 'Warranty', path: '/warranty-registration', icon: 'shield-check' }, { label: 'Download Catalogs', path: '/contact/request-literature', icon: 'download' } ] }, categories: [ { header: 'Infrared Heating Panels', header_link: '/infrared-heating-panels', card_layout: true, items: [ { label: 'Ember Glass', path: '/infrared-heating-panels/ember/glass', image: 'https://ik.warmlyyours.com/img/ember-glass-black-600-angle-8qtqsr.jpeg?tr=w-240,h-240,fo-auto', description: 'Sleek glass panels' }, { label: 'Ember Mirror', path: '/infrared-heating-panels/ember/mirror', image: 'https://ik.warmlyyours.com/img/ipe-emg-mw-6090-600-angle-xjvmqb.jpeg?tr=w-240,h-240,fo-auto', description: 'Reflective panels' }, { label: 'Ember Flex', path: '/infrared-heating-panels/ember/flex', image: 'https://ik.warmlyyours.com/img/ember-flex-white-400-angle-mqbsdp.jpeg?tr=w-240,h-240,fo-auto', description: 'Flexible panels' }, { label: 'Thermostats', path: '/infrared-heating-panels/controls', image: 'https://ik.warmlyyours.com/tr:w-240,h-240,fo-auto/img/nspire-touch-thermostat-angle-with-82-image-b3t9oe', description: 'Smart controls' } ] }, { header: 'Bathroom & Kitchen Comfort', card_layout: true, items: [ { label: 'LED Mirrors', path: '/led-mirror', image: 'https://ik.warmlyyours.com/img/01-marilyn-hero-67hazb?tr=w-240,h-240,fo-auto', description: 'Backlit elegance' }, { label: 'Mirror Defoggers', path: '/mirror-defogger', image: 'https://ik.warmlyyours.com/img/mirror-defogger-circle-6e15ac.jpeg?tr=w-240,h-240,fo-auto', description: 'Clear reflections' }, { label: 'Countertop Heaters', path: '/countertop-heater', image: 'https://ik.warmlyyours.com/img/countertop-heating-izycjr?tr=w-240,h-240,fo-auto', description: 'Warm surfaces' } ] } ] }, inspiration: { label: 'Inspiration', categories: [ { header: 'Learn & Discover', items: [ { label: 'Our Radiant Journal Blog', path: '/posts', icon: 'newspaper' }, { label: 'Watch Videos', path: '/videos', icon: 'circle-play' }, { label: 'Join Webinars & Live Events', path: '/webinar', icon: 'calendar' }, { label: 'Customer Reviews', path: '/reviews', icon: 'star' }, { label: 'Press Room', path: '/company/press', icon: 'bullhorn' } ] }, { header: 'Project Galleries', header_link: '/showcases', items: [ { label: 'Floor Heating Showcase', path: '/floor-heating/showcases' }, { label: 'Snow Melting Showcase', path: '/snow-melting/showcases' }, { label: 'All Showcases', path: '/showcases', icon: 'grid-2' } ] }, { header: 'Our Company', header_link: '/company', items: [ { label: 'Our Story', path: '/company' }, { label: 'Customer Testimonials', path: '/company/testimonials' }, { label: 'Brand Assets', path: '/brand-assets' } ] }, { header: 'Featured Blog', featured_card: true, image: 'https://ik.warmlyyours.com/img/installing-snow-melt-mats-on-asphalt-driveway-363c9c.jpeg?tr=w-400,h-260,fo-auto', title: 'Heated Driveway Cost Breakdown: What to Expect in 2025', path: '/posts/How-to-Calculate-the-Cost-of-a-Heated-Driveway-1181', cta: 'See Featured Blog' }, { header: 'Featured Gallery', featured_card: true, image: 'https://ik.warmlyyours.com/img/sls-warmly-yours-images-29-uafcat?tr=w-400,h-260,fo-auto', title: 'Radiant Luxury for a Farmhouse Master Bath', path: '/showcases/master-bathroom', cta: 'See Featured Gallery' } ] }, help_and_pros: { label: 'Help & Pros', categories: [ { header: 'Get Help', header_link: '/support', items: [ { label: 'My Account', path: '#offcanvas-account', offcanvas: true }, { label: 'Product Support', path: '/support' }, { label: 'Training & Tutorials', path: '/product-training' }, { label: 'Courses', path: '/courses' }, { label: 'Register Your Warranty', path: '/warranty-registration' }, { label: 'Services', path: '/services' } ] }, { header: 'Find & Connect', header_link: '/contact', items: [ { label: 'Find a Dealer or Installer', path: '/company/where-to-buy' }, { label: 'Contact Us', path: '/contact' }, { label: 'Hours of Operation', path: '/company/hours-of-operation' } ] }, { header: 'For Pros', header_link: '/trade', pro_column: true, items: [ { label: 'Join the Pro Program', path: '/trade', icon: 'badge-check' }, { label: 'Download Catalogs', path: '/contact/request-literature', icon: 'book' }, { label: 'See Upcoming Tradeshows', path: '/trade/trade-shows', icon: 'calendar' } ] } ] } }.freeze
Instance Method Summary collapse
-
#desktop_items(items) ⇒ Object
Filters items for desktop display (excludes mobile_only items).
-
#desktop_menu_link(item) ⇒ Object
Renders a desktop dropdown menu item with icon, badge, and image support.
-
#has_quick_links?(key) ⇒ Boolean
Checks if a menu has Quick Links.
-
#menu_item_badge(item) ⇒ Object
Renders a badge for menu items.
-
#menu_item_help(item) ⇒ Object
Renders a "?" help icon whose hover/focus tooltip carries the item's description.
-
#menu_item_icon(item, classes: 'me-2') ⇒ Object
Renders an icon for menu items.
-
#menu_item_url(item) ⇒ Object
Returns the URL for a menu item, handling both regular paths and publication downloads Publication items should have publication_skus: ['USA-SKU', 'CANADA-SKU'].
-
#mobile_image_url(url, compact: false) ⇒ Object
Returns a resized image URL for mobile menu (smaller for performance) Preserves cm-extract crops while replacing size transforms.
-
#mobile_items(items) ⇒ Object
Filters items for mobile display (excludes desktop_only items).
-
#mobile_menu_link(item, compact_image: false) ⇒ Object
Renders a mobile menu item link with icon, image and badge support.
-
#mobile_quick_link(item) ⇒ Object
Renders a mobile quick link with visible icon.
-
#navbar_column_class(_key) ⇒ Object
Auto-calculates column class based on number of categories.
-
#navbar_header_tag(category_or_section, css_class:) ⇒ Object
Renders a category/section header — as a link if header_link is present, otherwise a plain span.
-
#navbar_menu(key) ⇒ Object
Returns the menu structure for a given menu key.
-
#navbar_menu_keys ⇒ Object
Returns all menu keys in display order.
-
#publication_url_for_menu(*skus) ⇒ Object
Helper to get publication URL that works in both view and ViewComponent contexts Falls back to the catalogs page if publication is not found OPTIMIZATION: Uses Rails.cache to avoid repeated publication lookups across requests.
-
#quick_links_for(key) ⇒ Object
Returns the Quick Links column for a specific menu (if it has one).
-
#single_category_menu?(key) ⇒ Boolean
Checks if a menu has only one category (used to skip header on mobile).
Instance Method Details
#desktop_items(items) ⇒ Object
Filters items for desktop display (excludes mobile_only items)
531 532 533 |
# File 'app/helpers/www/navbar_helper.rb', line 531 def desktop_items(items) items.reject { |item| item[:mobile_only] } end |
#desktop_menu_link(item) ⇒ Object
Renders a desktop dropdown menu item with icon, badge, and image support
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 |
# File 'app/helpers/www/navbar_helper.rb', line 714 def (item) link_class = item[:highlight] ? 'dropdown-item dropdown-item-highlight' : 'dropdown-item' = { class: link_class } [:target] = '_blank' if item[:publication_skus] if item[:offcanvas] content = safe_join([ (item), item[:label], (item) ].compact) link_to content, item[:path], class: link_class, data: { 'bs-toggle': 'offcanvas' }, role: 'button' elsif item[:image] [:class] += ' d-flex align-items-center' link_to (item), ** do safe_join([ image_tag(item[:image], class: 'me-2', style: 'width: 20px; height: 20px; object-fit: cover;', alt: item[:image_alt]), item[:label], (item) ].compact) end else # `:subtitle` renders inline secondary text (used by Size / Bar Shape entries). # `:description` is the legacy key — it still drives the `?` tooltip via # `menu_item_help`. The two are mutually exclusive in practice. label_content = if item[:subtitle] tag.span(class: 'dropdown-item-label-wrap') do safe_join([ tag.span(item[:label], class: 'dropdown-item-label'), tag.span(item[:subtitle], class: 'dropdown-item-desc') ]) end else item[:label] end content = safe_join([ (item), label_content, (item), (item) ].compact) link_to content, (item), ** end end |
#has_quick_links?(key) ⇒ Boolean
Checks if a menu has Quick Links
506 507 508 |
# File 'app/helpers/www/navbar_helper.rb', line 506 def has_quick_links?(key) NAVBAR_MENUS[key][:quick_links].present? end |
#menu_item_badge(item) ⇒ Object
Renders a badge for menu items
536 537 538 539 540 541 |
# File 'app/helpers/www/navbar_helper.rb', line 536 def (item) return unless item[:badge] badge_class = item[:badge_class] || 'bg-success' tag.span(item[:badge], class: "badge #{badge_class} ms-1") end |
#menu_item_help(item) ⇒ Object
Renders a "?" help icon whose hover/focus tooltip carries the item's
description. Lets menu items (e.g. the plan finders) surface a short
explanation without cluttering the label. Desktop only — the mobile
menu renders the description inline (see #mobile_menu_link). Requires
the tooltip Stimulus controller on an ancestor element.
555 556 557 558 559 560 561 562 563 564 565 566 |
# File 'app/helpers/www/navbar_helper.rb', line 555 def (item) return if item[:description].blank? tag.span( fa_icon('circle-question', class: 'fa-fw'), class: 'menu-item-help text-muted ms-1', tabindex: 0, role: 'note', data: { 'bs-toggle': 'tooltip', 'bs-placement': 'right', 'bs-container': 'body', 'bs-title': item[:description] } ) end |
#menu_item_icon(item, classes: 'me-2') ⇒ Object
Renders an icon for menu items
544 545 546 547 548 |
# File 'app/helpers/www/navbar_helper.rb', line 544 def (item, classes: 'me-2') return unless item[:icon] fa_icon(item[:icon], class: "fa-fw #{classes}") end |
#menu_item_url(item) ⇒ Object
Returns the URL for a menu item, handling both regular paths and publication downloads
Publication items should have publication_skus: ['USA-SKU', 'CANADA-SKU']
653 654 655 656 657 658 659 |
# File 'app/helpers/www/navbar_helper.rb', line 653 def (item) if item[:publication_skus] (*item[:publication_skus]) else cms_link(item[:path]) end end |
#mobile_image_url(url, compact: false) ⇒ Object
Returns a resized image URL for mobile menu (smaller for performance)
Preserves cm-extract crops while replacing size transforms
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 |
# File 'app/helpers/www/navbar_helper.rb', line 611 def mobile_image_url(url, compact: false) size = compact ? '60' : '80' size_transform = "w-#{size},h-#{size},fo-auto" # Handle path-style transforms (tr:...) if url.include?('/tr:') # Check if URL has cm-extract crop that should be preserved if url =~ %r{/tr:(cm-extract[^,]*(?:,[xywh]-\d+)*)} extract_params = Regexp.last_match(1) url.gsub(%r{/tr:[^/]+/}, "/tr:#{extract_params},#{size_transform}/") else url.gsub(%r{/tr:[^/]+/}, "/tr:#{size_transform}/") end # Handle query-style transforms (?tr=...) elsif url.include?('?tr=') url.gsub(/\?tr=[^&]+/, "?tr=#{size_transform}") # No existing transform, add query param else "#{url}?tr=#{size_transform}" end end |
#mobile_items(items) ⇒ Object
Filters items for mobile display (excludes desktop_only items)
526 527 528 |
# File 'app/helpers/www/navbar_helper.rb', line 526 def mobile_items(items) items.reject { |item| item[:desktop_only] } end |
#mobile_menu_link(item, compact_image: false) ⇒ Object
Renders a mobile menu item link with icon, image and badge support
569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 |
# File 'app/helpers/www/navbar_helper.rb', line 569 def (item, compact_image: false) # Build the visual element (image or icon) visual = if item[:image] image_tag( mobile_image_url(item[:image], compact: compact_image), class: compact_image ? 'mobile-menu-image mobile-menu-image--compact' : 'mobile-menu-image', alt: item[:label], loading: 'lazy' ) elsif item[:icon] fa_icon(item[:icon], class: 'mobile-menu-icon') end # Build label with optional inline secondary text. Prefers `:subtitle` # (new explicit inline key) and falls back to `:description` for legacy # items where the desktop pattern uses a `?` tooltip but mobile renders it inline. secondary = item[:subtitle] || item[:description] label_content = if secondary tag.span(class: 'mobile-menu-label-wrap') do safe_join([ tag.span(item[:label], class: 'mobile-menu-label'), tag.span(secondary, class: 'mobile-menu-desc') ]) end else tag.span(item[:label], class: 'mobile-menu-label') end content = safe_join([visual, label_content, (item)].compact) link_class = item[:image] ? 'mobile-menu-link mobile-menu-link--has-image' : 'mobile-menu-link' = { class: link_class } [:data] = { 'bs-toggle': 'offcanvas' } if item[:offcanvas] [:role] = 'button' if item[:offcanvas] [:target] = '_blank' if item[:publication_skus] link_to content, (item[:offcanvas] ? item[:path] : (item)), end |
#mobile_quick_link(item) ⇒ Object
Renders a mobile quick link with visible icon
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 |
# File 'app/helpers/www/navbar_helper.rb', line 634 def mobile_quick_link(item) icon = (fa_icon(item[:icon], class: 'mobile-quick-link-icon') if item[:icon]) content = safe_join([ icon, tag.span(item[:label], class: 'mobile-quick-link-label'), (item) ].compact) = { class: 'mobile-quick-link' } [:data] = { 'bs-toggle': 'offcanvas' } if item[:offcanvas] [:role] = 'button' if item[:offcanvas] [:target] = '_blank' if item[:publication_skus] link_to content, (item[:offcanvas] ? item[:path] : (item)), end |
#navbar_column_class(_key) ⇒ Object
Auto-calculates column class based on number of categories
511 512 513 |
# File 'app/helpers/www/navbar_helper.rb', line 511 def (_key) 'col' end |
#navbar_header_tag(category_or_section, css_class:) ⇒ Object
Renders a category/section header — as a link if header_link is present, otherwise a plain span
702 703 704 705 706 707 708 709 710 711 |
# File 'app/helpers/www/navbar_helper.rb', line 702 def (category_or_section, css_class:) header = category_or_section[:header] link = category_or_section[:header_link] if link link_to header, cms_link(link), class: css_class else tag.span header, class: css_class end end |
#navbar_menu(key) ⇒ Object
Returns the menu structure for a given menu key
496 497 498 |
# File 'app/helpers/www/navbar_helper.rb', line 496 def (key) NAVBAR_MENUS[key] end |
#navbar_menu_keys ⇒ Object
Returns all menu keys in display order
516 517 518 |
# File 'app/helpers/www/navbar_helper.rb', line 516 def NAVBAR_MENUS.keys end |
#publication_url_for_menu(*skus) ⇒ Object
Helper to get publication URL that works in both view and ViewComponent contexts
Falls back to the catalogs page if publication is not found
OPTIMIZATION: Uses Rails.cache to avoid repeated publication lookups across requests.
Cache key includes store_id for locale-specific publications.
665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 |
# File 'app/helpers/www/navbar_helper.rb', line 665 def (*skus) # In ViewComponent, we need to access helpers through the helpers proxy store = respond_to?(:helpers) ? helpers.locale_store : locale_store store_id = store&.id || 1 # Create a cache key based on the SKUs and store cache_key = "navbar_publication_url/#{store_id}/#{skus.join('-')}" # Use Rails cache with 1-hour expiration (publications rarely change) cached_sku = Rails.cache.fetch(cache_key, expires_in: 1.hour) do # Find the first matching publication found_pub = nil skus.each do |sku| pub = Item.find_publication(sku, store) if pub found_pub = pub.sku break end end found_pub # Cache the SKU (or nil if not found) end if cached_sku return helpers.www_publication_path(cached_sku, format: :pdf) if respond_to?(:helpers) return www_publication_path(cached_sku, format: :pdf) end # Fallback to catalogs page if publication not found cms_link('/contact/request-literature') rescue StandardError => e Rails.logger.warn "Error resolving publication URL for #{skus}: #{e.}" cms_link('/contact/request-literature') end |
#quick_links_for(key) ⇒ Object
Returns the Quick Links column for a specific menu (if it has one)
501 502 503 |
# File 'app/helpers/www/navbar_helper.rb', line 501 def quick_links_for(key) NAVBAR_MENUS[key][:quick_links] end |
#single_category_menu?(key) ⇒ Boolean
Checks if a menu has only one category (used to skip header on mobile)
521 522 523 |
# File 'app/helpers/www/navbar_helper.rb', line 521 def (key) NAVBAR_MENUS[key][:categories].size == 1 end |