1window.ItchJamWidget = (function() { 2 'use strict'; 3 4 function getCache(key) { 5 if (window.ApiRateLimiter) { 6 return window.ApiRateLimiter.getCache(key); 7 } 8 try { 9 var data = localStorage.getItem(key); 10 if (!data) return null; 11 var parsed = JSON.parse(data); 12 if (parsed && parsed.value && Object.keys(parsed.value).length > 0) { 13 if (Date.now() - parsed.timestamp < (60 * 60 * 1000)) { 14 return parsed.value; 15 } 16 } 17 } catch(e) {} 18 return null; 19 } 20 21 function setCache(key, value) { 22 if (window.ApiRateLimiter) { 23 window.ApiRateLimiter.setCache(key, value); 24 return; 25 } 26 try { 27 if (!value || Object.keys(value).length === 0) return; 28 localStorage.setItem(key, JSON.stringify({ 29 timestamp: Date.now(), 30 value: value 31 })); 32 } catch(e) {} 33 } 34 35 function clearCache(key) { 36 if (window.ApiRateLimiter) { 37 window.ApiRateLimiter.clearCache(key); 38 return; 39 } 40 try { 41 localStorage.removeItem(key); 42 } catch(e) {} 43 } 44 45 function renderLoading(element) { 46 if (!element) return; 47 element.innerHTML = '<span class="stat-chip stat-loading" title="Loading live jam stats..."><span class="stat-icon" style="-webkit-mask-image: url(\'/icons/hourglass.svg\'); mask-image: url(\'/icons/hourglass.svg\');"></span></span>'; 48 } 49 50 function renderError(element, onRetry) { 51 if (!element) return; 52 element.innerHTML = '<span class="stat-chip stat-error" title="Data Couldn\'t Load (Right-click to retry)"><span class="stat-icon" style="-webkit-mask-image: url(\'/icons/xmark.svg\'); mask-image: url(\'/icons/xmark.svg\');"></span></span>'; 53 54 var errChip = element.querySelector('.stat-error'); 55 if (errChip && typeof onRetry === 'function') { 56 errChip.addEventListener('contextmenu', function(e) { 57 e.preventDefault(); 58 e.stopPropagation(); 59 onRetry(); 60 }); 61 } 62 } 63 64 function makeIcon(file) { 65 return '<span class="stat-icon" style="-webkit-mask-image: url(\'/icons/' + file + '\'); mask-image: url(\'/icons/' + file + '\');"></span>'; 66 } 67 68 function render(element, stats) { 69 if (!element || !stats) return; 70 71 var cleanHost = (stats.host || '').replace(/skip to main content/gi, '').trim(); 72 73 // Client-side date check safeguard for status 74 var effectiveStatus = stats.status; 75 var endDateStr = stats.voting_end_date || stats.end_date; 76 if (endDateStr) { 77 try { 78 var endDate = new Date(endDateStr.replace(' ', 'T') + (endDateStr.includes('T') ? '' : 'Z')); 79 if (!isNaN(endDate.getTime()) && Date.now() > endDate.getTime()) { 80 effectiveStatus = 'Ended'; 81 } 82 } catch(e) {} 83 } 84 85 var html = ''; 86 if (cleanHost && element.id !== 'jam-modal-live-stats') { 87 html += '<span class="stat-chip stat-host" title="Organizer(s) / Host(s)">' + makeIcon('user.svg') + ' ' + cleanHost + '</span>'; 88 } 89 if (stats.entries) { 90 html += '<span class="stat-chip stat-entries" title="Jam Entries (Games)">' + makeIcon('gamepad.svg') + ' ' + stats.entries + ' entries</span>'; 91 } 92 if (stats.ratings) { 93 html += '<span class="stat-chip stat-ratings" title="Total Ratings Received">' + makeIcon('star.svg') + ' ' + stats.ratings + ' ratings</span>'; 94 } 95 if (stats.rating_comments) { 96 html += '<span class="stat-chip stat-rating-comments" title="Rating Comments & Feedback">' + makeIcon('comment.svg') + ' ' + stats.rating_comments + ' feedback comments</span>'; 97 } 98 if (stats.submissions) { 99 html += '<span class="stat-chip stat-submissions" title="Submissions Count">' + makeIcon('file-code.svg') + ' ' + stats.submissions + ' submissions</span>'; 100 } 101 if (stats.joined) { 102 html += '<span class="stat-chip stat-joined" title="Participants / Joined">' + makeIcon('users.svg') + ' ' + stats.joined + ' joined</span>'; 103 } 104 if (stats.comments && stats.comments !== '0' && !stats.rating_comments) { 105 html += '<span class="stat-chip stat-comments" title="Comments">' + makeIcon('comment.svg') + ' ' + stats.comments + ' comments</span>'; 106 }
107 if ((stats.start_date || stats.end_date) && element.id !== 'jam-modal-live-stats') { 108 var dateStr = (stats.start_date && stats.end_date) ? (stats.start_date.split(' ')[0] + ' to ' + stats.end_date.split(' ')[0]) : (stats.start_date || stats.end_date); 109 html += '<span class="stat-chip stat-dates" title="Jam Timeline Dates">' + makeIcon('calendar.svg') + ' ' + dateStr + '</span>'; 110 } 111 if (effectiveStatus) { 112 html += '<span class="stat-chip stat-status" title="Jam Status">' + makeIcon('clock.svg') + ' ' + effectiveStatus + '</span>'; 113 } 114 115 element.innerHTML = html; 116 117 if (element.id === 'jam-modal-live-stats') { 118 var modalHost = document.getElementById('jam-modal-host'); 119 var modalDates = document.getElementById('jam-modal-dates'); 120 if (modalHost && cleanHost) { 121 modalHost.textContent = cleanHost; 122 } 123 if (modalDates && (stats.start_date || stats.end_date)) { 124 var dateRange = (stats.start_date && stats.end_date) ? (stats.start_date.split(' ')[0] + ' to ' + stats.end_date.split(' ')[0]) : (stats.start_date || stats.end_date); 125 modalDates.textContent = dateRange; 126 } 127 } 128 } 129 130 function fetchItchJamStats(jam, onSuccess, onError) { 131 var baseUrl = (window.ApiRateLimiter && window.ApiRateLimiter.getApiBaseUrl) ? window.ApiRateLimiter.getApiBaseUrl() : ''; 132 var proxyUrl = baseUrl + '/api/proxy/itch_jam?jam=' + jam; 133 var taskFn = function() { 134 return fetch(proxyUrl) 135 .then(function(res) { 136 if (!res.ok) throw new Error('HTTP ' + res.status); 137 return res.json(); 138 }); 139 }; 140 141 if (window.ApiRateLimiter) { 142 window.ApiRateLimiter.enqueue(proxyUrl, taskFn, onSuccess, onError); 143 } else { 144 taskFn().then(onSuccess).catch(onError); 145 } 146 } 147 148 function initElement(el) { 149 var jam = el.getAttribute('data-itch-jam'); 150 if (!jam) return; 151 152 var cacheKey = 'solvin_itch_jam_stats_v1_' + jam; 153 var cached = getCache(cacheKey); 154 if (cached && Object.keys(cached).length > 0) { 155 render(el, cached); 156 return; 157 } 158 159 renderLoading(el); 160 161 var baseUrl = (window.ApiRateLimiter && window.ApiRateLimiter.getApiBaseUrl) ? window.ApiRateLimiter.getApiBaseUrl() : ''; 162 var proxyUrl = baseUrl + '/api/proxy/itch_jam?jam=' + jam; 163 164 function handleRetry() { 165 clearCache(cacheKey); 166 if (window.ApiRateLimiter) { 167 window.ApiRateLimiter.resetDomain(proxyUrl); 168 } 169 170 renderLoading(el); 171 fetchItchJamStats(jam, function(stats) { 172 if (stats && Object.keys(stats).length > 0) { 173 setCache(cacheKey, stats); 174 render(el, stats); 175 } else { 176 renderError(el, handleRetry); 177 } 178 }, function(err) { 179 renderError(el, handleRetry); 180 }); 181 } 182 183 fetchItchJamStats(jam, function(stats) { 184 if (stats && Object.keys(stats).length > 0) { 185 setCache(cacheKey, stats); 186 render(el, stats); 187 } else { 188 renderError(el, handleRetry); 189 } 190 }, function(err) { 191 renderError(el, handleRetry); 192 }); 193 } 194 195 function init() { 196 var jamElements = document.querySelectorAll('[data-itch-jam]'); 197 jamElements.forEach(function(el) { 198 if (el.classList.contains('fav-collection-item-chip') || (el.closest && el.closest('.fav-collection-item-chip'))) return; 199 initElement(el); 200 }); 201 } 202 203 return { 204 init: init, 205 initElement: initElement, 206 fetchItchJamStats: fetchItchJamStats 207 }; 208})();
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.