1<!DOCTYPE html> 2<html lang="en"> 3<head> 4<meta charset="UTF-8"> 5<meta name="viewport" content="width=device-width, initial-scale=1.0"> 6<title>N.B.S.V.A | Official Verified Blood Strike Vendors in Nigeria</title> 7<meta name="description" content="The Nigeria Blood Strike Vendors Association â the official organization for verified Blood Strike vendors. View vendor profiles, ratings and reviews, and verify any seller before you deal."> 8<meta name="keywords" content="Blood Strike Vendor Nigeria, Verified Blood Strike Vendors, Blood Strike Vendor Association, NBSVA, Trusted Blood Strike Vendors, Blood Strike Nigeria"> 9<link rel="canonical" href="https://nbsva.web.app/"> 10<meta property="og:title" content="N.B.S.V.A â Official Verified Blood Strike Vendors in Nigeria"> 11<meta property="og:description" content="Trade safe. Deal smart. Stay protected. Verify any seller before you deal."> 12<meta property="og:type" content="website"> 13<meta property="og:url" content="https://nbsva.web.app/"> 14<meta property="og:image" content="https://nbsva.web.app/assets/social-banner.png"> 15<meta property="og:image:width" content="1983"> 16<meta property="og:image:height" content="793"> 17<meta name="twitter:card" content="summary_large_image"> 18<meta name="twitter:title" content="N.B.S.V.A â Official Verified Blood Strike Vendors in Nigeria"> 19<meta name="twitter:description" content="Trade safe. Deal smart. Stay protected. Verify any seller before you deal."> 20<meta name="twitter:image" content="https://nbsva.web.app/assets/social-banner.png"> 21<link rel="icon" type="image/x-icon" href="/favicon.ico"> 22<link rel="icon" type="image/png" sizes="192x192" href="/assets/icon-192.png"> 23<link rel="icon" type="image/png" sizes="512x512" href="/assets/icon-512.png"> 24<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png"> 25<meta name="mobile-web-app-capable" content="yes"> 26<meta name="apple-mobile-web-app-capable" content="yes"> 27<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> 28<meta name="apple-mobile-web-app-title" content="N.B.S.V.A"> 29<link rel="manifest" href="manifest.json"> 30<meta name="theme-color" content="#070707"> 31<link rel="preconnect" href="https://fonts.googleapis.com"> 32<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 33<link rel="preconnect" href="https://the-hunt-scrims.firebaseio.com" crossorigin> 34<link rel="preconnect" href="https://www.gstatic.com" crossorigin> 35<link rel="preload" as="script" href="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js" crossorigin> 36<link rel="preload" as="script" href="https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js" crossorigin> 37<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@700;800;900&family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> 38<link rel="stylesheet" href="css/style.css"> 39<link rel="stylesheet" href="css/responsive.css">
40<script type="application/ld+json"> 41{ 42 "@context": "https://schema.org", 43 "@type": "Organization", 44 "name": "The Nigeria Blood Strike Vendors Association", 45 "alternateName": "NBSVA", 46 "url": "https://nbsva.web.app/", 47 "logo": "https://nbsva.web.app/assets/logo.png", 48 "foundingDate": "2024", 49 "slogan": "Trade Safe. Deal Smart. Stay Protected.", 50 "description": "The official organization for verified Blood Strike vendors in Nigeria.", 51 "areaServed": "NG" 52} 53</script>
53 54</head> 55<body class="home-v2"> 56<div id="nav-wrapper"></div> 57 58<!-- ============ HERO ============ --> 59<section class="hero v2-hero"> 60 <div class="container"> 61 <div class="v2-hero-split"> 62 <div class="v2-hero-copy"> 63 <span class="v2-est-pill"><span class="v2-est-dot"></span> EST. 2024</span> 64 <h1 class="v2-hero-title"> 65 Trade Safe.<br> 66 Deal Smart.<br> 67 <span class="v2-red">Stay Protected.</span> 68 </h1> 69 <p class="v2-hero-lede">The #1 Verified Vendor Association for the Blood Strike community in Nigeria.</p> 70 <div class="v2-hero-actions"> 71 <a href="#check" class="v2-btn v2-btn-red">Instant Vendor Check</a> 72 <a href="/contact" class="v2-btn v2-btn-outline">Report a Scammer</a> 73 </div> 74 </div> 75 <div class="v2-hero-art"> 76 <div class="v2-hero-glow"></div> 77 <img src="assets/logo.png" alt="" class="v2-hero-watermark"> 78 <img src="assets/hero-hitman.png" alt="N.B.S.V.A" class="v2-hero-char" onerror="this.style.display='none'"> 79 </div> 80 81 </div> 82 </div> 83</section> 84 85<!-- ============ LATEST BLACKLISTED TICKER ============ --> 86<div class="enf-ticker v2-ticker" id="enfTicker" style="display:none"> 87 <div class="container v2-ticker-row"> 88 <a href="/enforcement" id="enfTickerLink">⚠ Latest blacklisted: …</a> 89 <a href="/enforcement" class="v2-ticker-more">View All Enforcement →</a> 90 </div> 91</div> 92 93<!-- ============ LIVE COMMUNITY STATS ============ --> 94<section class="section tight v2-stats-section"> 95 <div class="container"> 96 <div class="v2-live-label"><span class="v2-live-dot grn"></span> Live community stats <span class="v2-live-sub">Real-time from our database</span></div> 97 <div class="stats-grid v2-stats-grid"> 98 <div class="stat v2-stat v2-glow-grn"><span class="icon">✅</span><div class="num grn" id="stVendors" data-count="0">0</div><div class="label">Verified Vendors</div><a href="/vendors" class="stat-link">View all vendors →</a></div> 99 <div class="stat v2-stat v2-glow-red"><span class="icon">🚫</span><div class="num rd" id="stEnforcement" data-count="0">0</div><div class="label">Enforcement Entries</div><a href="/enforcement" class="stat-link">View all entries →</a></div> 100 <div class="stat v2-stat v2-glow-gld"><span class="icon">⭐</span><div class="num gld" id="stReviews" data-count="0">0</div><div class="label">Reviews Posted</div><a href="/vendors" class="stat-link">View all reviews →</a></div> 101 <div class="stat v2-stat v2-glow-blu"><span class="icon">🛡️</span><div class="num blu" id="stResolved" data-count="0">0</div><div class="label">Reports Resolved</div><a href="/enforcement" class="stat-link">View reports →</a></div> 102 </div> 103 </div> 104</section> 105 106<!-- ============ LIVE ENFORCEMENT FEED (carousel) ============ --> 107<section class="section tight" id="feedSection" style="display:none;padding-bottom:-1px"> 108 <div class="container"> 109 <div class="feed-carousel-wrap v2-feed-wrap"> 110 <div class="feed-carousel-head"> 111 <span class="live-dot v2-live-label"><span class="v2-live-dot red"></span> Live enforcement feed <span class="v2-live-sub">Stay informed. Stay safe.</span></span> 112 <div class="feed-carousel-arrows"> 113 <button type="button" onclick="feedScroll(-1)" aria-label="Previous">←</button> 114 <button type="button" onclick="feedScroll(1)" aria-label="Next">→</button> 115 </div> 116 </div> 117 <div class="feed-carousel-track" id="feedTrack"></div> 118 <div class="feed-carousel-dots" id="feedDots"></div> 119 </div> 120 </div> 121</section> 122 123<!-- ============ HOW IT WORKS ============ --> 124<section class="section tight band v2-how"> 125 <div class="container"> 126 <div class="section-head"> 127 <span class="label-caps">Three simple steps to trade safely</span> 128 <h2 class="v2-h2">How it works</h2> 129 </div> 130 <div class="how-steps-v2"> 131 <div class="how-step-v2 v2-step"> 132 <div class="how-step-v2-icon grn"><span class="how-step-v2-num" style="color:var(--green)">1</span>🔍</div> 133 <div><h3>Check the vendor</h3><p>Verify their profile, trust score and reviews before you deal.</p></div> 134 </div> 135 <div class="how-arrow">→</div> 136 <div class="how-step-v2 v2-step"> 137 <div class="how-step-v2-icon blu"><span class="how-step-v2-num" style="color:var(--blue)">2</span>💬</div> 138 <div><h3>Read their reviews</h3><p>See what other buyers are saying about their track record.</p></div> 139 </div> 140 <div class="how-arrow">→</div> 141 <div class="how-step-v2 v2-step"> 142 <div class="how-step-v2-icon ppl"><span class="how-step-v2-num" style="color:#c084fc">3</span>✅</div> 143 <div><h3>Deal safely</h3><p>Make smart, secure transactions â or use a Verified Vendor as middleman.</p></div> 144 </div> 145 </div> 146 </div> 147</section> 148 149<!-- ============ INSTANT VENDOR CHECK ============ --> 150<section class="section tight" id="check"> 151 <div class="container"> 152 <div class="verify-box v2-verify"> 153 <h3 style="margin-bottom:6px">Instant Vendor Check</h3> 154 <p style="color:var(--muted);font-size:.86rem;line-height:1.6;margin-bottom:14px">Type a seller's name or WhatsApp number to confirm they're officially verified â before you deal.</p> 155 <div class="verify-row"> 156 <input type="text" id="qvInput" placeholder="e.g. JMAROXS or 234815..." style="flex:1;background:#0b0b0b;border:1px solid var(--line);border-radius:10px;padding:13px 14px;color:var(--text);font-size:.95rem" onkeydown="if(event.key==='Enter')quickVerify()">
157 <button class="v2-btn v2-btn-red" style="padding:12px 20px" onclick="quickVerify()">Check</button> 158 </div> 159 <div class="verify-result" id="qvResult"></div> 160 </div> 161 </div> 162</section> 163 164<!-- ============ RECENT REVIEWS ============ --> 165<section class="section tight"> 166 <div class="container"> 167 <div id="recentReviewsSection" class="v2-duo-reviews v2-reviews-full"> 168 <div class="v2-panel-head"> 169 <div class="v2-panel-label">Recent reviews</div> 170 <a href="/vendors" class="v2-viewall">View All →</a> 171 </div> 172 <div class="reviews-strip" id="recentReviewsBox"> 173 <div class="review-tile review-skeleton"><div class="rv-skel-stars"></div><div class="rv-skel-line l1"></div><div class="rv-skel-line l2"></div><div class="rv-skel-who"></div></div> 174 <div class="review-tile review-skeleton"><div class="rv-skel-stars"></div><div class="rv-skel-line l1"></div><div class="rv-skel-line l2"></div><div class="rv-skel-who"></div></div> 175 <div class="review-tile review-skeleton"><div class="rv-skel-stars"></div><div class="rv-skel-line l1"></div><div class="rv-skel-line l2"></div><div class="rv-skel-who"></div></div> 176 </div> 177 </div> 178 </div> 179</section> 180 181<!-- ============ TOP 5 VENDOR LEADERBOARD ============ --> 182<section class="section tight" id="leaderboardSection"> 183 <div class="container"> 184 <div class="v2-lb"> 185 <div class="v2-panel-head"> 186 <div class="v2-panel-label gold">🏆 Top 5 vendor leaderboard</div> 187 <a href="/vendors" class="v2-viewall">Full Directory →</a> 188 </div> 189 <div id="leaderBox"> 190 <div class="lb-row lb-skeleton"><span class="lb-rank">1</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div> 191 <div class="lb-row lb-skeleton"><span class="lb-rank">2</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div> 192 <div class="lb-row lb-skeleton"><span class="lb-rank">3</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div> 193 <div class="lb-row lb-skeleton"><span class="lb-rank">
1934</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div> 194 <div class="lb-row lb-skeleton"><span class="lb-rank">5</span><div class="lb-skel-avatar"></div><div class="lb-who"><div class="lb-skel-name"></div><div class="lb-skel-sub"></div></div><div class="lb-skel-trust"></div></div> 195 </div> 196 </div> 197 </div> 198</section> 199 200<div class="divider container"></div> 201 202<!-- ============ RECENT ENFORCEMENT ALERTS ============ --> 203<section class="section tight"> 204 <div class="container"> 205 <div class="section-head"> 206 <span class="label-caps" style="color:var(--red2)">Trust & Safety</span> 207 <h2 class="v2-h2">Recent enforcement alerts</h2> 208 <p>Impersonators and confirmed scammers the association has caught. Tap one to see the evidence.</p> 209 </div> 210 <div id="homeScams" class="enf-grid"></div> 211 <div style="margin-top:26px"> 212 <a href="/enforcement" class="v2-btn v2-btn-outline">View Full Enforcement List</a> 213 </div> 214 </div> 215</section> 216 217<!-- ============ BROWSE BY CATEGORY ============ --> 218<section class="section tight"> 219 <div class="container"> 220 <div class="section-head"> 221 <span class="label-caps">Browse by category</span> 222 <h2 class="v2-h2">Find vendors based on what you need</h2> 223 </div> 224 <div class="cat-tile-grid" id="homeCatTiles"></div> 225 <div style="margin-top:24px"> 226 <a href="/vendors" class="v2-btn v2-btn-outline">View All Vendors →</a> 227 </div> 228 </div> 229</section> 230 231<div class="divider container"></div> 232 233<!-- ============ TOGETHER BANNER ============ --> 234<section class="section tight"> 235 <div class="container"> 236 <div class="together-banner v2-together"> 237 <div class="v2-together-copy"> 238 <img src="assets/logo.png" alt="" class="v2-together-crest" loading="lazy"> 239 <h2 class="v2-together-title">Together, we build a <span class="v2-red">safer</span> community</h2> 240 <p>Your safety is our priority. Report scammers, leave honest reviews, and help protect others.</p> 241 <div class="v2-hero-actions"> 242 <a href="/contact" class="v2-btn v2-btn-red">⚠ Report a Scammer</a> 243 <a href="/become-a-vendor" class="v2-btn v2-btn-outline">👤 Become a Vendor</a> 244 </div> 245 </div> 246 <div class="together-banner-art v2-together-art"> 247 <img src="assets/hero-hitman.png" alt="" loading="lazy" onerror="this.parentElement.style.display='none'"> 248 </div> 249 </div> 250 </div> 251</section> 252 253<!-- ============ NEWSLETTER ============ --> 254<section class="section tight"> 255 <div class="container"> 256 <div class="newsletter-box v2-newsletter"> 257 <div style="display:flex;align-items:center;gap:16px;flex:1;min-width:240px"> 258 <span class="nl-icon">📧</span> 259 <div> 260 <h3>Stay <span class="v2-red">Updated</span></h3> 261 <p>Subscribe to get the latest enforcement alerts, safety tips and platform updates.</p> 262 </div> 263 </div> 264 <form class="newsletter-form" onsubmit="return subscribeNewsletter(event)"> 265 <input type="email" id="nlEmail" placeholder="Enter your email address" required> 266 <button type="submit" class="v2-btn v2-btn-red">Subscribe</button> 267 </form> 268 <p class="newsletter-hint">🔒 No spam. Unsubscribe anytime.</p> 269 </div> 270 </div> 271</section> 272 273<!-- ============ FEATURE STRIP ============ --> 274<section class="section tight" style="padding-top:0"> 275 <div class="container"> 276 <div class="feature-strip v2-features"> 277 <div class="feature-strip-item"><span class="icon">👥</span><div><h4>Stronger Community</h4><p>Chat, support and trust all in one place.</p></div></div> 278 <div class="feature-strip-item"><span class="icon">📊</span><div><h4>Transparency First</h4><p>Real reviews, real vendors, real results.</p></div></div> 279 <div class="feature-strip-item"><span class="icon">🛡️</span><div><h4>Safer Trading</h4><p>We protect you from scammers and fraud.</p></div></div> 280 <div class="feature-strip-item"><span class="icon">🇳🇬</span><div><h4>Built for Nigeria</h4><p>Proudly serving the Blood Strike community.</p></div></div> 281 </div> 282 </div> 283</section> 284 285<div id="footer-wrapper"></div>
286<script src="js/nav.js"></script>
vendor: 1 bytes, line 286
286
287<script crossorigin="anonymous" src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
vendor: 1 bytes, line 287
287
288<script crossorigin="anonymous" src="https://www.gstatic.com/firebasejs/9.22.0/firebase-database-compat.js"></script>
vendor: 1 bytes, line 288
288
289<script crossorigin="anonymous" src="https://www.gstatic.com/firebasejs/9.22.0/firebase-functions-compat.js"></script>
vendor: 1 bytes, line 289
289
290<script>var NBSVA_NEEDS = ['vendors', 'scams', 'reviews', 'reports'];</script>
vendor: 1 bytes, line 290
290
291<script src="js/firebase-init.js"></script>
vendor: 1 bytes, line 291
291
292<script src="js/data.js"></script>
vendor: 1 bytes, line 292
292
293<script src="js/reviews.js"></script>
vendor: 1 bytes, line 293
293
294<script src="js/catalog.js"></script>
vendor: 1 bytes, line 294
294
295<script src="js/main.js"></script>
vendor: 1 bytes, line 295
295
296<script src="js/nova-client.js"></script>
vendor: 1 bytes, line 296
296
297<script> 298function renderHomeScams() { 299 var box = document.getElementById('homeScams'); 300 if (!box) return; 301 box.innerHTML = allScams().slice(0, 3).map(scamCard).join('') || '<p style="color:var(--muted);text-align:center;grid-column:1/-1">No enforcement records yet.</p>'; 302} 303renderHomeScams(); 304 305/* Category tiles â same regex matching as the Vendor Directory's category 306 chips (vendors.html), so a vendor counted here shows up under the same 307 category if you tap through to /vendors. */ 308var HOME_CAT_TILES = [ 309 { label: 'Accounts', icon: '👤', color: 'grn', re: /account/i }, 310 { label: 'Top-ups', icon: '💳', color: 'blu', re: /top-?up|\bcp\b|gems|gold|coin/i }, 311 { label: 'Passes', icon: '🎟', color: 'gld', re: /pass/i }, 312 { label: 'Recovery/Unlinking', icon: '🔒', color: 'rd', re: /recover|unlink|unban|unbind/i }, 313 { label: 'Escrow/MM', icon: '🛡️', color: 'blu', re: /escrow|middleman|\bmm\b/i } 314]; 315var CAT_TILE_COLORS = { grn: { bg: 'var(--green-soft)', fg: 'var(--green)' }, blu: { bg: 'rgba(29,155,240,.12)', fg: 'var(--blue)' }, gld: { bg: 'rgba(245,179,1,.12)', fg: 'var(--gold)' }, rd: { bg: 'rgba(240,68,56,.12)', fg: 'var(--red)' } }; 316function renderHomeCatTiles() { 317 var vendors = allVendors(); 318 document.getElementById('homeCatTiles').innerHTML = HOME_CAT_TILES.map(function (c) { 319 var n = vendors.filter(function (v) { return c.re.test(v.sells || ''); }).length; 320 var col = CAT_TILE_COLORS[c.color]; 321 return '<a href="/vendors" class="cat-tile"><div class="cat-tile-icon" style="background:' + col.bg + ';color:' + col.fg + '">' + c.icon + '</div>' 322 + '<div style="font-weight:700;font-size:.92rem">' + c.label + '</div>' 323 + '<div class="cat-tile-count">' + n + '+ Vendors</div></a>'; 324 }).join(''); 325} 326renderHomeCatTiles(); 327 328/* Newsletter â no email backend exists yet, so this saves the address to 329 /newsletterSignups for staff to export later rather than silently 330 discarding it or pretending to send a real confirmation email. */ 331function subscribeNewsletter(e) { 332 e.preventDefault(); 333 var input = document.getElementById('nlEmail'); 334 var email = input.value.trim(); 335 if (!email) return false; 336 if (typeof fbDB === 'undefined') { showToast('Could not save â try again shortly.', 'err'); return false; } 337 fbDB.ref('newsletterSignups').push({ email: email, ts: Date.now() }).then(function () { 338 showToast('Subscribed! You\'ll get enforcement alerts and updates.', 'ok'); 339 input.value = ''; 340 }).catch(function () { 341 showToast('Could not subscribe right now â try again later.', 'err'); 342 }); 343 return false; 344} 345 346/* Stat numbers are computed from live data, never hand-typed. Setting 347 data-count before DOMContentLoaded lets the existing count-up animation 348 (animateCounters, in main.js) play with the right value on first load;
349 later live updates (a new vendor/scam added) just set the number directly. */ 350function updateHomeStats(live) { 351 var vendors = allVendors(); 352 var scams = allScams(); 353 var reviewCount = (typeof _rvCache !== 'undefined') 354 ? Object.keys(_rvCache).reduce(function (sum, vid) { return sum + _rvCache[vid].length; }, 0) : 0; 355 // "Resolved" = reports that went through the vendor-report review flow and were 356 // approved (reportedBy.verified) â the only outcome a public visitor can read; 357 // rejected reports live in /rejectedScams, which is staff-only for privacy. 358 var resolvedCount = scams.filter(function (s) { return s.reportedBy && s.reportedBy.verified; }).length; 359 [['stVendors', vendors.length], ['stEnforcement', scams.length], ['stReviews', reviewCount], ['stResolved', resolvedCount]].forEach(function (pair) { 360 var el = document.getElementById(pair[0]); 361 if (!el) return; 362 el.dataset.count = pair[1]; 363 if (live) el.textContent = pair[1]; 364 }); 365} 366updateHomeStats(false); 367 368/* Rotates through the 5 most recently blacklisted entries â same public 369 record set as enforcement.html (staff/vendor scam reports; vendor-status 370 blacklists don't carry a `date` precise enough for "x days ago", so the 371 ticker sticks to actual scam reports, which always have one). */ 372var _tickerTimer = null, _tickerIdx = 0; 373function daysAgo(dateStr) { 374 var d = Date.parse(dateStr); 375 if (isNaN(d)) return ''; 376 var days = Math.max(0, Math.floor((Date.now() - d) / 86400000)); 377 return days === 0 ? 'today' : days === 1 ? '1 day ago' : days + ' days ago'; 378} 379function renderTicker() { 380 var entries = allScams().filter(function (s) { return s.status === 'Blacklisted'; }) 381 .sort(function (a, b) { return (b.date || '').localeCompare(a.date || ''); }).slice(0, 5); 382 var box = document.getElementById('enfTicker'); 383 if (!entries.length) { box.style.display = 'none'; if (_tickerTimer) clearInterval(_tickerTimer); return; } 384 box.style.display = 'block'; 385 var link = document.getElementById('enfTickerLink'); 386 function show(i) { 387 var s = entries[i % entries.length]; 388 link.innerHTML = '⚠ Latest blacklisted: <strong>' + escapeHtml(s.name) + '</strong> — ' + escapeHtml(s.type || '') + ' — ' + daysAgo(s.date); 389 link.href = s.id ? '/scam-detail?id=' + encodeURIComponent(s.id) : '/enforcement'; 390 } 391 show(0); _tickerIdx = 0; 392 if (_tickerTimer) clearInterval(_tickerTimer); 393 if (entries.length > 1) _tickerTimer = setInterval(function () { _tickerIdx++; show(_tickerIdx); }, 5000); 394} 395renderTicker(); 396 397/* Live Enforcement Feed carousel â same entries as the ticker above (any 398 status, not just Blacklisted, so Suspended/Under Appeal show too), 399 rendered as scrollable cards instead of rotating text. */ 400function feedStatusClass(status) { return status === 'Blacklisted' ? 'tag-red' : status === 'Under Appeal' ? 'tag-blue' : 'tag-gold'; } 401function renderFeedCarousel() { 402 var section = document.getElementById('feedSection'); 403 var entries = allScams().sort(function (a, b) { return (b.date || '').localeCompare(a.date || ''); }).slice(0, 8); 404 if (!entries.length) { section.style.display = 'none'; return; } 405 section.style.display = 'block'; 406 document.getElementById('feedTrack').innerHTML = entries.map(function (s) { 407 var href = s.id ? '/scam-detail?id=' + encodeURIComponent(s.id) : '/enforcement'; 408 var initial = (s.name || '?').charAt(0).toUpperCase(); 409 return '<a href="' + href + '" class="feed-card">' 410 + '<div class="feed-card-top">' 411 + (s.photo ? '<img src="' + s.photo + '" class="feed-card-avatar" alt="">' : '<div class="feed-card-avatar">' + initial + '</div>') 412 + '<div style="min-width:0"><span class="tag ' + feedStatusClass(s.status) + '" style="font-size:.64rem;padding:2px 8px">' + escapeHtml(s.status) + '</span>' 413 + '<div class="feed-card-name">' + escapeHtml(s.name) + '</div></div>' 414 + '</div>' 415 + '<div class="feed-card-meta">' + escapeHtml(s.type || '') + ' • ' + daysAgo(s.date) + '</div>' 416 + '</a>'; 417 }).join(''); 418 var dots = document.getElementById('feedDots'); 419 dots.innerHTML = entries.map(function (_, i) { return '<span' + (i === 0 ? ' class="active"' : '') + '></span>'; }).join(''); 420 var track = document.getElementById('feedTrack'); 421 track.onscroll = function () { 422 var cardWidth = 222; // .feed-card flex-basis (210px) + gap (12px) 423 var idx = Math.round(track.scrollLeft / cardWidth); 424 Array.prototype.forEach.call(dots.children, function (d, i) { d.classList.toggle('active', i === idx); }); 425 }; 426} 427function feedScroll(dir) { 428 var track = document.getElementById('feedTrack'); 429 track.scrollBy({ left: dir * 222, behavior: 'smooth' }); 430} 431renderFeedCarousel(); 432 433/* Recent Reviews â always visible; skeleton â live tiles â empty state. */ 434function renderRecentReviews() { 435 var section = document.getElementById('recentReviewsSection'); 436 if (!section) return; 437 section.style.display = 'block'; 438 var box = document.getElementById('recentReviewsBox'); 439 if (!box) return; 440 441 if (typeof _rvCache === 'undefined') return; // keep skeleton until Firebase loads 442 var all = []; 443 Object.keys(_rvCache).forEach(function (vid) {
444 _rvCache[vid].forEach(function (r) { all.push({ r: r, vendorId: vid }); }); 445 }); 446 all.sort(function (a, b) { return (b.r.ts || 0) - (a.r.ts || 0); }); 447 var recent = all.slice(0, 5); 448 449 if (!recent.length) { 450 box.innerHTML = '<div class="reviews-empty"><span>💬</span><p>No reviews yet â they\'ll show up here as buyers leave them.</p></div>'; 451 return; 452 } 453 454 box.innerHTML = recent.map(function (item) { 455 var v = allVendors(true, true).find(function (x) { return x.id === item.vendorId; }); 456 var vname = v ? v.name : 'a vendor'; 457 return '<div class="review-tile" onclick="location.href=\'/vendors?v=' + encodeURIComponent(item.vendorId) + '\'">' 458 + '<span class="stars">' + starsOf(item.r.stars) + '</span>' 459 + '<p>“' + escapeHtml(item.r.text) + '”</p>' 460 + '<div class="who">— ' + escapeHtml(item.r.name) + ' on ' + escapeHtml(vname) + '</div>' 461 + '</div>'; 462 }).join(''); 463} 464renderRecentReviews(); 465 466var _homeRefresh = window._nbsvaRefresh; 467window._nbsvaRefresh = function () { 468 if (_homeRefresh) _homeRefresh(); 469 renderHomeScams(); updateHomeStats(true); renderTicker(); renderFeedCarousel(); renderRecentReviews(); renderHomeCatTiles(); renderLeaderboard(); 470}; 471 472/* Ranking must never be computed from cache state that just HAPPENS to be 473 empty because Firebase hasn't answered yet â _rvCache = {} on page load 474 looks identical to "these vendors genuinely have 0 reviews" (see 475 ratingSummary()/trustScore() in js/main.js), so without an explicit 476 "did the real data arrive" signal this would render a ranking built off 477 a snapshot that's still empty by default, not by fact. 478 _reviewsLoaded/_vendorsLoaded (js/firebase-init.js) are only set true 479 inside the real on('value') callbacks, so gating on them (not on cache 480 size) is what actually distinguishes "not loaded" from "loaded, and 481 there's nothing there". */ 482var _lbWaitStart = Date.now(); 483var LEADERBOARD_TIMEOUT_MS = 15000; 484function renderLeaderboard() { 485 var box = document.getElementById('leaderBox'); 486 if (!box || typeof allVendors === 'undefined') return; 487 488 if (!_reviewsLoaded || !_vendorsLoaded) { 489 if (Date.now() - _lbWaitStart > LEADERBOARD_TIMEOUT_MS) { 490 box.innerHTML = '<p style="color:var(--muted);text-align:center;padding:20px 0">Couldnât load rankings â check your connection and reload the page.</p>'; 491 } 492 return; // keep the skeleton (or the timeout message above) â never a guessed ranking 493 } 494 495 var ranked = allVendors().filter(function (v) { 496 return (v.status || 'verified') === 'verified'; 497 }).map(function (v) { 498 var t = trustScore(v.id), r = ratingSummary(v.id); 499 return { v: v, score: t.score, avg: r.avg, count: r.count }; 500 }).sort(function (a, b) {
501 return (b.score - a.score) || (b.count - a.count) || (b.avg - a.avg); 502 }).slice(0, 5); 503 if (!ranked.length) { document.getElementById('leaderboardSection').style.display = 'none'; return; } 504 box.innerHTML = ranked.map(function (row, i) { 505 var v = row.v; 506 var medal = i === 0 ? '\uD83E\uDD47' : i === 1 ? '\uD83E\uDD48' : i === 2 ? '\uD83E\uDD49' : '#' + (i + 1); 507 return '<div class="lb-row' + (i === 0 ? ' first' : '') + '" onclick="location.href=\'/vendors?v=' + encodeURIComponent(v.id) + '\'">' 508 + '<span class="lb-rank">' + medal + '</span>' 509 + vendorAvatar(v, 40) 510 + '<div class="lb-who"><span class="lb-name">' + v.name + ' <span class="vcheck">✓</span></span>' 511 + '<span class="lb-sub">' + (row.count ? starsOf(row.avg) + ' (' + row.count + ')' : 'No reviews yet') + '</span></div>' 512 + '<div class="lb-trust"><span class="l">Trust</span><span class="n">' + row.score + '%</span></div>' 513 + '</div>'; 514 }).join(''); 515} 516renderLeaderboard(); 517/* Firebase only re-triggers renderLeaderboard() (via _nbsvaRefresh above) 518 when a listener actually delivers a snapshot â if one never fires at all 519 (offline, blocked request, no network error callback), nothing would 520 ever re-check the timeout and the skeleton would spin forever instead of 521 showing the "couldn't load" message. This one-shot check fires once, 522 after the timeout window, purely to catch that case. */ 523setTimeout(renderLeaderboard, LEADERBOARD_TIMEOUT_MS + 500); 524</script>
524 525</body> 526</html>
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.