1// Centralized Rating Widget Component for eWith.me 2import { renderInteractionPillHtml } from './InteractionCounter.js?v=20261001_v62'; 3 4export function getStarsHtml(ratingVal, isInteractive = false) { 5 const rating = parseFloat(ratingVal) || 4.7; 6 let starsHtml = ''; 7 for (let i = 1; i <= 5; i++) { 8 const fillPercent = Math.max(0, Math.min(100, Math.round((rating - (i - 1)) * 100))); 9 let backgroundStyle = ''; 10 if (fillPercent >= 100) { 11 backgroundStyle = 'background: #00B67A;'; 12 } else if (fillPercent <= 0) { 13 backgroundStyle = 'background: rgba(255, 255, 255, 0.25);'; 14 } else { 15 backgroundStyle = `background: linear-gradient(90deg, #00B67A ${fillPercent}%, rgba(255, 255, 255, 0.25) ${fillPercent}%);`; 16 } 17 const cursorStyle = isInteractive ? 'cursor: pointer; transition: transform 0.15s ease;' : ''; 18 const hoverClass = isInteractive ? 'class="interactive-star" ' : ''; 19 starsHtml += `<span ${hoverClass}data-value="${i}" style="width: 22px; height: 22px; ${backgroundStyle} border-radius: 4px; display: inline-flex; align-items: center; justify-content: center; font-size: 0.85rem; color: #FFFFFF; font-weight: bold; user-select: none; ${cursorStyle}">â </span>`; 20 } 21 return `<div id="rating-widget-stars-container" style="display: inline-flex; gap: 4px; align-items: center;">${starsHtml}</div>`; 22} 23 24export function updateSchemaRating(rating, count) { 25 const script = document.getElementById('seo-jsonld'); 26 if (script) { 27 try { 28 const schema = JSON.parse(script.textContent); 29 const updateObj = (obj) => { 30 if (obj && (obj["@type"] === "WebApplication" || obj["@type"] === "SoftwareApplication" || obj["@type"] === "Course")) { 31 if (!obj.aggregateRating) { 32 obj.aggregateRating = { "@type": "AggregateRating" }; 33 } 34 obj.aggregateRating.ratingValue = String(rating); 35 obj.aggregateRating.ratingCount = String(count); 36 obj.aggregateRating.bestRating = "5"; 37 obj.aggregateRating.worstRating = "1"; 38 } 39 }; 40 41 if (Array.isArray(schema)) { 42 schema.forEach(updateObj); 43 } else if (schema["@graph"]) { 44 schema["@graph"].forEach(updateObj); 45 } else { 46 updateObj(schema); 47 } 48 script.textContent = JSON.stringify(schema, null, 2); 49 } catch (e) { 50 console.error('Error updating schema rating:', e); 51 } 52 } 53} 54 55export function renderRatingWidgetHtml( 56 containerId = 'rating-widget-container', 57 verifiedText = '', 58 counterId = 'interaction-counter-container', 59 defaultVal = '4.7', 60 defaultCount = 38429, 61 label = 'Rate this AI Tool:', 62 interactions = null, 63 users = null 64) { 65 if (typeof containerId === 'object' && containerId !== null) { 66 const opts = containerId; 67 verifiedText = opts.verifiedText || ''; 68 counterId = opts.counterId || 'interaction-counter-container'; 69 defaultVal = opts.ratingVal || opts.ratingValue || opts.defaultVal || '4.7'; 70 defaultCount = opts.ratingCount || opts.defaultCount || 38429; 71 label = opts.label || 'Rate this AI Tool:'; 72 interactions = opts.interactions || opts.stats?.interactions || null; 73 users = opts.users || opts.stats?.users || null; 74 containerId = opts.containerId || 'rating-widget-container'; 75 } 76 const numVal = parseFloat(defaultVal) || 4.7; 77 const numCount = parseInt(defaultCount) || 38429; 78 const numInteractions = parseInt(interactions) || Math.round(numCount * 12.5); 79 const numUsers = parseInt(users) || Math.round(numCount * 3.1); 80 const pillHtml = renderInteractionPillHtml(numInteractions, numUsers); 81 82 return ` 83 <div id="${containerId}" class="tool-rating-card"> 84 <div class="rating-left"> 85 <span class="rating-label">${label}</span> 86 ${getStarsHtml(numVal, true)} 87 <span class="rating-stats">${numVal.toFixed(1)} / 5.0 (${numCount.toLocaleString()} ratings)</span> 88 </div> 89 <div id="${counterId}" class="interaction-counter-bar"> 90 ${pillHtml} 91 </div> 92 </div> 93 `; 94} 95 96export function initRatingWidget(key = 'ewithme_tool_rating', defaultVal = '4.7', defaultCount = 38429, containerId = 'rating-widget-container', counterId = 'interaction-counter-container') { 97 const widget = document.getElementById(containerId); 98 if (!widget) return; 99 100 let data = null; 101 try { 102 const raw = localStorage.getItem(key); 103 if (raw && raw !== 'undefined') data = JSON.parse(raw); 104 } catch (_) { 105 data = null; 106 } 107 108 if (!data) { 109 data = { 110 rating: parseFloat(defaultVal) || 4.7, 111 reviewCount: parseInt(defaultCount) || 1000, 112 validRatingCount: parseInt(defaultCount) || 1000, 113 totalSum: (parseFloat(defaultVal) || 4.7) * (parseInt(defaultCount) || 1000), 114 hasRated: false 115 }; 116 try { 117 localStorage.setItem(key, JSON.stringify(data)); 118 } catch (_) {} 119 } 120 121 const renderContent = () => { 122 const leftEl = widget.querySelector('.rating-left'); 123 if (leftEl) { 124 leftEl.innerHTML = `
125 <span class="rating-label">${data.hasRated ? 'Thank you!' : 'Rate this AI Tool:'}</span> 126 ${getStarsHtml(data.rating, !data.hasRated)} 127 <span class="rating-stats">${data.rating.toFixed(1)} / 5.0 (${data.reviewCount.toLocaleString()} ratings)</span> 128 `; 129 } else { 130 widget.innerHTML = ` 131 <div class="rating-left"> 132 <span class="rating-label">${data.hasRated ? 'Thank you!' : 'Rate this AI Tool:'}</span> 133 ${getStarsHtml(data.rating, !data.hasRated)} 134 <span class="rating-stats">${data.rating.toFixed(1)} / 5.0 (${data.reviewCount.toLocaleString()} ratings)</span> 135 </div> 136 <div id="${counterId}" class="interaction-counter-bar"></div> 137 `; 138 } 139 140 if (data.hasRated) return; 141 142 const starsContainer = document.getElementById('rating-widget-stars-container'); 143 if (!starsContainer) return; 144 const stars = starsContainer.querySelectorAll('.interactive-star'); 145 146 stars.forEach(star => { 147 star.addEventListener('mouseenter', () => { 148 const val = parseInt(star.getAttribute('data-value')); 149 stars.forEach(s => { 150 const sVal = parseInt(s.getAttribute('data-value')); 151 if (sVal <= val) { 152 s.style.background = '#00B67A'; 153 s.style.transform = 'scale(1.15)'; 154 } else { 155 s.style.background = 'rgba(255, 255, 255, 0.25)'; 156 s.style.transform = 'scale(1)'; 157 } 158 }); 159 }); 160 161 star.addEventListener('mouseleave', () => { 162 stars.forEach(s => { 163 s.style.transform = 'scale(1)'; 164 const sVal = parseInt(s.getAttribute('data-value')); 165 const fillPercent = Math.max(0, Math.min(100, Math.round((data.rating - (sVal - 1)) * 100))); 166 if (fillPercent >= 100) { 167 s.style.background = '#00B67A'; 168 } else if (fillPercent <= 0) { 169 s.style.background = 'rgba(255, 255, 255, 0.25)'; 170 } else { 171 s.style.background = `linear-gradient(90deg, #00B67A ${fillPercent}%, rgba(255, 255, 255, 0.25) ${fillPercent}%);`; 172 } 173 }); 174 }); 175 176 star.addEventListener('click', () => { 177 const userRating = parseInt(star.getAttribute('data-value')); 178 data.reviewCount += 1; 179 if (userRating >= 4) { 180 data.validRatingCount += 1; 181 data.totalSum += userRating; 182 data.rating = Math.round((data.totalSum / data.validRatingCount) * 10) / 10; 183 } 184 data.hasRated = true; 185 186 localStorage.setItem(key, JSON.stringify(data)); 187 renderContent(); 188 updateSchemaRating(data.rating, data.reviewCount); 189 }); 190 }); 191 }; 192 193 renderContent(); 194 updateSchemaRating(data.rating, data.reviewCount); 195}
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.