1/** 2 * Homepage Reviews Slider 3 * Data source adapter for SliderUniversalV2. 4 * Uses reviewsData from clients-ranking-data.js and renders brand-refresh cards. 5 * 6 * Load order: 7 * 1. slider-universal.js 8 * 2. clients-ranking/clients-ranking-data.js 9 * 3. This file (reviews-slider.js) 10 */ 11 12/* ââ Star SVG helper ââ */ 13function homepageReviewsGenerateStars(rating) { 14 const starSvg = '<svg height="17" viewBox="0 0 16.75 16" xmlns="http://www.w3.org/2000/svg"><path d="M6.7,5.86c-.11.22-.32.38-.57.42l-3.73.55,2.7,2.63c.18.17.26.43.22.67l-.64,3.72,3.34-1.76c.22-.12.49-.12.71,0l3.34,1.76-.64-3.72c-.04-.25.04-.5.22-.67l2.7-2.63-3.73-.55c-.25-.04-.46-.19-.57-.42l-1.67-3.38-1.67,3.38ZM5.52,4.83L7.69.42c.28-.57,1.09-.57,1.36,0l2.17,4.41,4.86.71c.62.09.87.86.42,1.3l-3.52,3.43.83,4.84c.11.62-.55,1.1-1.1.8l-4.35-2.29-4.35,2.29c-.56.29-1.21-.18-1.1-.8l.83-4.84L.23,6.84c-.45-.44-.2-1.21.42-1.3l4.86-.71Z" fill="#00d672"/><path d="M6.7,5.86c-.11.22-.32.38-.57.42l-3.73.55,2.7,2.63c.18.17.26.43.22.67l-.64,3.72,3.34-1.76c.22-.12.49-.12.71,0l3.34,1.76-.64-3.72c-.04-.25.04-.5.22-.67l2.7-2.63-3.73-.55c-.25-.04-.46-.19-.57-.42l-1.67-3.38-1.67,3.38Z" fill="#00d672"/></svg>'; 15 let stars = ''; 16 for (let i = 0; i < Math.min(Math.round(rating), 5); i++) { 17 stars += starSvg; 18 } 19 return stars; 20} 21 22/* ââ Date formatter ââ */ 23function homepageReviewsFormatDate(dateStr, lang) { 24 if (!dateStr) return ''; 25 const parts = /^(\d{4})-(\d{2})-(\d{2})/.exec(dateStr); 26 if (!parts) return ''; 27 const date = new Date(Number(parts[1]), Number(parts[2]) - 1, Number(parts[3])); 28 const formatted = date.toLocaleDateString(lang === 'fr' ? 'fr-CA' : 'en-CA', { 29 day: '2-digit', 30 month: 'long', 31 year: 'numeric' 32 }); 33 // Capitalize the first letter (e.g. "01 juin 2026" -> "01 Juin 2026") 34 return formatted.replace(/(^|[^a-zA-Z])([a-zA-Z])/, function (match, before, letter) { 35 return before + letter.toUpperCase(); 36 }); 37} 38 39/* ââ Location formatter ââ */ 40function homepageReviewsFormatLocation(loc) { 41 if (!loc) return ''; 42 return String(loc).trim().replace(/[a-zA-ZÃ-ÿ]+/g, function (word) { 43 return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); 44 }); 45} 46 47/* ââ Review filtering / sorting helpers ââ */ 48var REVIEW_REGION_MAP = { 49 QC: 'quebec', 50 ON: 'ontario', 51 MB: 'manitoba', 52 AB: 'alberta', 53 BC: 'britishcolumbia' 54}; 55 56function normalizeRegionString(s) { 57 return (s || '').toLowerCase() 58 .normalize('NFD') 59 .replace(/[\u0300-\u036f]/g, '') 60 .replace(/[-\s]/g, ''); 61} 62 63function getLocationTokens(loc) { 64 if (!loc) return []; 65 return String(loc).split(',').map(function (part) { return normalizeRegionString(part); }); 66} 67 68function parseRegionFilter(regionAttr) { 69 if (!regionAttr || regionAttr === 'ANY') return null; 70 return regionAttr.split(/\s+/).map(function (term) { 71 var upper = term.toUpperCase(); 72 return REVIEW_REGION_MAP[upper] || normalizeRegionString(term); 73 }); 74} 75 76function reviewMatchesRegion(location, regionTerms) { 77 if (!regionTerms || regionTerms.length === 0) return true; 78 var tokens = getLocationTokens(location); 79 for (var i = 0; i < regionTerms.length; i++) { 80 for (var j = 0; j < tokens.length; j++) { 81 if (tokens[j] === regionTerms[i]) return true; 82 } 83 } 84 return false; 85} 86 87function sortReviewsForDisplay(reviews, order) { 88 order = (order || '').toLowerCase().trim(); 89 90 if (order.indexOf('latest') === 0) { 91 return reviews.slice().sort(function (a, b) { 92 return (b.date || '').localeCompare(a.date || ''); 93 }); 94 } 95 96 if (order === 'random') { 97 var shuffled = reviews.slice(); 98 for (var i = shuffled.length - 1; i > 0; i--) { 99 var j = Math.floor(Math.random() * (i + 1)); 100 var tmp = shuffled[i]; 101 shuffled[i] = shuffled[j]; 102 shuffled[j] = tmp; 103 } 104 return shuffled; 105 } 106 107 return reviews.slice(); 108} 109 110/* ââ Data source registration ââ */ 111function registerHomepageReviewsDataSource() {
112 var containers = document.querySelectorAll('[data-slider="homepageReviews"]'); 113 if (!containers.length) return; 114 115 containers.forEach(function (container, index) { 116 var maxReviews = parseInt(container.getAttribute('data-review-count'), 10) || 3; 117 if (maxReviews < 1) maxReviews = 3; 118 119 var order = (container.getAttribute('data-review-order') || '').toLowerCase().trim(); 120 var regionAttr = (container.getAttribute('data-review-region') || '').toUpperCase().trim(); 121 var regionTerms = parseRegionFilter(regionAttr); 122 var pageLang = (document.documentElement.lang || '').toLowerCase().split('-')[0]; 123 124 var filteredReviews = reviewsData.filter(function (r) { 125 var content = pageLang === 'fr' ? (r.contentFR || r.content) : r.content; 126 return reviewMatchesRegion(r.location, regionTerms) && content && content.trim() !== ''; 127 }); 128 129 var cappedReviews = sortReviewsForDisplay(filteredReviews, order).slice(0, maxReviews); 130 131 var regionSuffix = (container.id || String(index)).replace(/^homepage-reviews-/, ''); 132 var sourceName = 'homepageReviews-' + (regionSuffix || index); 133 134 window.SliderUniversalV2.registerDataSource(sourceName, cappedReviews, { 135 136 gridClass: 'slider-grid', 137 dotsClass: 'slider-nav-dots', 138 dotClass: 'slider-dot', 139 itemClass: 'slider-item', 140 141 breakpoints: { xl: 1200, lg: 992, md: 768, sm: 576, xs: 400 }, 142 itemsPerBreakpoint: { xl: 3, lg: 3, md: 2, sm: 1, xs: 1, default: 1 }, 143 144 peekAmount: 0.1, 145 gap: 16, 146 147 filterItems: function(item, lang) { 148 var content = lang === 'fr' ? (item.contentFR || item.content) : item.content; 149 return content && content.trim() !== ''; 150 }, 151 152 renderItem: function(item, lang) { 153 var title = lang === 'fr' ? (item.titleFR || item.title) : item.title; 154 var content = lang === 'fr' ? (item.contentFR || item.content) : item.content; 155 var date = homepageReviewsFormatDate(item.date, lang); 156 var location = homepageReviewsFormatLocation(item.location); 157 var stars = homepageReviewsGenerateStars(item.rating); 158 159 var el = document.createElement('div'); 160 el.innerHTML = 161 '<div class="card card-sm bg-surface-neutral-high radius-xl" style="height:100%;">' + 162 '<div class="card-body" style="display:flex;flex-direction:column;">' + 163 '<div class="d-flex gap-0-25 mb-3">' + stars + '</div>' + 164 '<p><strong>' + title + '</strong></p>' + 165 '<p style="flex:1;">' + content + '</p>' + 166 '<p class="mb-0" style="margin-top:auto;"><strong>' + item.author + '</strong><br>' + 167 '<small>' + (location ? location + ' | ' : '') + date + '</small></p>' + 168 '</div>' + 169 '</div>'; 170 return el; 171 } 172 }); 173 174 container.setAttribute('data-slider', sourceName); 175 }); 176} 177 178function renderHomepageReviewsSlider() { 179 if (typeof window.SliderUniversalV2 === 'undefined') return; 180 var containers = document.querySelectorAll('[data-slider^="homepageReviews-"]'); 181 containers.forEach(function (container) { 182 window.SliderUniversalV2.refresh(container); 183 }); 184} 185 186/* ââ Wait for dependencies, then register + render ââ 187 Handles cases where clients-ranking-data.js and/or slider-universal.js 188 load after this script (e.g. Drupal library aggregation order). */ 189function initHomepageReviewsWhenReady(retries) { 190 retries = retries || 0; 191 var maxRetries = 40; // ~4s at 100ms intervals 192 193 var sliderReady = typeof window.SliderUniversalV2 !== 'undefined'; 194 var dataReady = typeof reviewsData !== 'undefined'; 195 196 if (sliderReady && dataReady) { 197 registerHomepageReviewsDataSource(); 198 renderHomepageReviewsSlider(); 199 return; 200 } 201 202 if (retries >= maxRetries) { 203 if (!sliderReady) { 204 console.error('SliderUniversalV2 must be loaded before reviews-sli
204der.js'); 205 } 206 if (!dataReady) { 207 console.error('reviewsData not found â load clients-ranking-data.js before reviews-slider.js'); 208 } 209 return; 210 } 211 212 setTimeout(function () { 213 initHomepageReviewsWhenReady(retries + 1); 214 }, 100); 215} 216 217if (document.readyState === 'loading') { 218 document.addEventListener('DOMContentLoaded', function () { 219 initHomepageReviewsWhenReady(); 220 }); 221} else { 222 initHomepageReviewsWhenReady(); 223}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.