PageSourceSearch

https://ewith.me/components/RatingWidget.js?v=20261001_v62

js ewith.me collected 2026-10-02 02:00:42 UTC 7,725 bytes, 195 lines download raw bytes

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.