1// ============================================================ 2// SportFan.ro â Main JavaScript (Public Site) 3// FoloseÈte Firestore REST API (funcÈioneazÄ Èi din file://) 4// ============================================================ 5 6const SF_PROJECT = 'sportfan-e33d9'; 7const SF_API_KEY = 'AIzaSyAsT3nhmeG_MLU7QUn0d_fTaKmQP9t3MNM'; 8const SF_BASE = `https://firestore.googleapis.com/v1/projects/${SF_PROJECT}/databases/(default)/documents`; 9 10// ââ Firestore REST Helpers ââââââââââââââââââââââââââââââ 11function parseVal(v) { 12 if (!v) return null; 13 if (v.stringValue !== undefined) return v.stringValue; 14 if (v.booleanValue !== undefined) return v.booleanValue; 15 if (v.integerValue !== undefined) return parseInt(v.integerValue); 16 if (v.doubleValue !== undefined) return parseFloat(v.doubleValue); 17 if (v.nullValue !== undefined) return null; 18 if (v.timestampValue !== undefined) { 19 const d = new Date(v.timestampValue); 20 return { toDate: () => d, toMillis: () => d.getTime() }; 21 } 22 if (v.arrayValue !== undefined) 23 return (v.arrayValue.values || []).map(parseVal); 24 if (v.mapValue !== undefined) 25 return parseFields(v.mapValue.fields || {}); 26 return null; 27} 28 29function parseFields(fields) { 30 const obj = {}; 31 for (const [k, v] of Object.entries(fields || {})) obj[k] = parseVal(v); 32 return obj; 33} 34 35function docToArticle(docObj) { 36 const id = docObj.name.split('/').pop(); 37 return { id, ...parseFields(docObj.fields) }; 38} 39 40// ââ Utilities âââââââââââââââââââââââââââââââââââââââââââ 41function formatDate(ts) { 42 if (!ts) return ''; 43 const d = ts.toDate ? ts.toDate() : new Date(ts); 44 const now = new Date(); 45 const diff = Math.floor((now - d) / 1000); 46 if (diff < 60) return 'Acum câteva secunde'; 47 if (diff < 3600) return `Acum ${Math.floor(diff / 60)} min`; 48 if (diff < 86400) return `Acum ${Math.floor(diff / 3600)}h`; 49 return d.toLocaleDateString('ro-RO', { day: '2-digit', month: '2-digit', year: 'numeric' }); 50} 51 52function catLabel(cat) { 53 return { fotbal: 'Fotbal', tenis: 'Tenis', baschet: 'Baschet', stiri_actuale: 'Ètiri Actuale', altele: 'Altele' }[cat] || (cat || 'Altele'); 54} 55 56// ââ Article Card HTML âââââââââââââââââââââââââââââââââââ 57function renderCard(art) { 58 const cat = art.category ? art.category.toLowerCase() : "altele"; 59 const urlCat = cat === 'stiri_actuale' ? 'stiri-actuale' : cat; 60 const url = `/${urlCat}/${art.slug || art.id}`; 61 const image = art.image || `https://picsum.photos/seed/${art.id}/400/250`; 62 return ` 63 <div class="article-card" onclick="location.href='${url}'"> 64 <div class="card-img"> 65 <img src="${image}" alt="${art.title}" loading="lazy" 66 onerror="this.src='https://picsum.photos/seed/${art.id}x/400/250'"> 67 </div> 68 <div class="card-body"> 69 <div class="card-category cat-${cat}"> 70 <span class="cat-dot cat-dot-${cat}"></span>${catLabel(cat)} 71 </div> 72 <div class="card-title">${art.title}</div> 73 <div class="card-meta"> 74 <span class="date">ð ${formatDate(art.createdAt)}</span> 75 <span class="views">ðï¸ ${art.views || 0}</span> 76 </div> 77 ${art.author ? `<div style="font-size:11px;color:#9aa0b4;margin-top:4px">âï¸ ${art.author}</div>` : ''} 78 </div> 79 </div>`; 80} 81 82// ââ Hero Article HTML âââââââââââââââââââââââââââââââââââ 83function renderHero(art) { 84 const cat = art.category ? art.category.toLowerCase() : "altele"; 85 const urlCat = cat === 'stiri_actuale' ? 'stiri-actuale' : cat; 86 const url = `/${urlCat}/${art.slug || art.id}`; 87 const image = art.image || `https://picsum.photos/seed/${art.id}/1200/500`; 88 return ` 89 <div class="hero-article" onclick="location.href='${url}'"> 90 <img src="${image}" alt="${art.title}" 91 onerror="this.src='https://picsum.photos/seed/hero/1200/500'"> 92 <div class="hero-overlay">
93 <span class="hero-badge cat-badge-${cat}">${catLabel(cat)}</span> 94 <div class="hero-title">${art.title}</div> 95 <div class="hero-meta"> 96 <span>âï¸ ${art.author || 'RedacÈie'}</span> 97 <span>ð ${formatDate(art.createdAt)}</span> 98 <span>ðï¸ ${art.views || 0} vizualizÄri</span> 99 </div> 100 </div> 101 </div>`; 102} 103 104// ââ List Item HTML (sidebar) ââââââââââââââââââââââââââââ 105function renderListItem(art) { 106 const cat = art.category ? art.category.toLowerCase() : "altele"; 107 const urlCat = cat === 'stiri_actuale' ? 'stiri-actuale' : cat; 108 const url = `/${urlCat}/${art.slug || art.id}`; 109 const image = art.image || `https://picsum.photos/seed/${art.id}/160/120`; 110 return ` 111 <div class="article-list-item" onclick="location.href='${url}'"> 112 <div class="ali-img"> 113 <img src="${image}" alt="${art.title}" loading="lazy" 114 onerror="this.src='https://picsum.photos/seed/${art.id}r/160/120'"> 115 </div> 116 <div class="ali-body"> 117 <div class="ali-title">${art.title}</div> 118 <div class="ali-date cat-${art.category}">${catLabel(art.category)} ⢠${formatDate(art.createdAt)}</div> 119 </div> 120 </div>`; 121} 122 123// ââ Skeleton Placeholder ââââââââââââââââââââââââââââââââ
124function renderSkeletons(n, gridClass = '') { 125 return `<div class="articles-grid ${gridClass}">` + 126 Array(n).fill('<div class="skeleton skeleton-card"></div>').join('') + 127 '</div>'; 128} 129 130// ââ Fetch All Published Articles (REST API + cache) âââââ 131let _cache = null; 132let _cacheTime = 0; 133const CACHE_MS = 5 * 60 * 1000; // 5 minutes 134 135async function fetchAllArticles() { 136 if (_cache && Date.now() - _cacheTime < CACHE_MS) return _cache; 137 138 // PRIMARY: Simple GET â works from file:// (no CORS preflight) 139 try { 140 const res = await fetch( 141 `${SF_BASE}/articles?key=${SF_API_KEY}&pageSize=100` 142 ); 143 if (!res.ok) throw new Error(`HTTP ${res.status}`); 144 const data = await res.json(); 145 _cache = (data.documents || []) 146 .map(d => docToArticle(d)) 147 .filter(a => a.published !== false) 148 .sort((a, b) => { 149 const ta = a.createdAt?.toMillis ? a.createdAt.toMillis() : 0; 150 const tb = b.createdAt?.toMillis ? b.createdAt.toMillis() : 0; 151 return tb - ta; 152 }); 153 _cacheTime = Date.now(); 154 return _cache; 155 } catch (e) { 156 console.warn('GET eÈuat, încerc runQuery:', e); 157 } 158 159 // FALLBACK: runQuery POST (funcÈioneazÄ de pe server HTTP) 160 try { 161 const res = await fetch(`${SF_BASE}:runQuery?key=${SF_API_KEY}`, { 162 method: 'POST', 163 headers: { 'Content-Type': 'application/json' }, 164 body: JSON.stringify({ 165 structuredQuery: { 166 from: [{ collectionId: 'articles' }], 167 orderBy: [{ field: { fieldPath: 'createdAt' }, direction: 'DESCENDING' }], 168 limit: 100 169 } 170 }) 171 }); 172 if (!res.ok) throw new Error(`HTTP ${res.status}`); 173 const rows = await res.json(); 174 _cache = rows 175 .filter(r => r.document) 176 .map(r => docToArticle(r.document)) 177 .filter(a => a.published !== false); 178 _cacheTime = Date.now(); 179 return _cache; 180 } catch (e) { 181 console.error('Ambele metode au eÈuat:', e); 182 return []; 183 } 184} 185 186// ââ Fetch Single Article by Firestore ID (REST API) ââââ 187async function fetchArticle(id) { 188 const res = await fetch(`${SF_BASE}/articles/${id}?key=${SF_API_KEY}`); 189 if (!res.ok) throw new Error(`HTTP ${res.status}`); 190 const doc = await res.json(); 191 return docToArticle(doc); 192} 193 194// ââ Fetch Single Article by Slug (Firestore SDK) ââââââââ 195async function fetchArticleBySlug(slug) { 196 const db = firebase.firestore(); 197 const snap = await db.collection('articles') 198 .where('slug', '==', slug) 199 .limit(1) 200 .get(); 201 if (snap.empty) throw new Error('Articolul nu a fost gÄsit.'); 202 const doc = snap.docs[0]; 203 return { id: doc.id, ...doc.data() }; 204} 205 206// ââ Increment Views (REST API PATCH) ââââââââââââââââââââ 207async function incrementViews(id) { 208 try { 209 // Use Firestore runQuery to get current views, then PATCH 210 const art = await fetchArticle(id); 211 const newViews = (art.views || 0) + 1; 212 await fetch( 213 `${SF_BASE}/articles/${id}?key=${SF_API_KEY}&updateMask.fieldPaths=views`, 214 { 215 method: 'PATCH', 216 headers: { 'Content-Type': 'application/json' }, 217 body: JSON.stringify({ fields: { views: { integerValue: String(newViews) } } }) 218 } 219 ); 220 } catch (e) { /* silent */ } 221} 222 223// ââ Load Featured Article âââââââââââââââââââââââââââââââ 224async function loadFeatured(targetId) { 225 const el = document.getElementById(targetId); 226 if (!el) return; 227 el.innerHTML = '<div class="skeleton skeleton-hero"></div>'; 228 try { 229 const arts = await fetchAllArticles(); 230 const featured = arts.find(a => a.featured) || arts[0]; 231 if (!featured) { 232 el.innerHTML = `<div class="empty-state"> 233 <div class="icon">ð°</div> 234 <p>Nu existÄ articole. AccesaÈi <a href="setup.html">setup.html</a> pentru date demo.</p> 235 </div>`; 236 return; 237 } 238 el.innerHTML = renderHero(featured); 239 } catch (e) { el.innerHTML = '';
239 console.error(e); } 240} 241 242// ââ Load Articles Grid ââââââââââââââââââââââââââââââââââ 243async function loadArticles({ targetId, category, limit = 8, exclude = null, gridClass = '' }) { 244 const el = document.getElementById(targetId); 245 if (!el) return; 246 el.innerHTML = renderSkeletons(Math.min(limit, 4), gridClass); 247 try { 248 const arts = await fetchAllArticles(); 249 const filtered = arts 250 .filter(a => !category || a.category === category) 251 .filter(a => a.id !== exclude) 252 .slice(0, limit); 253 254 if (!filtered.length) { 255 el.innerHTML = `<div class="empty-state"> 256 <div class="icon">ð°</div> 257 <p>${category ? `Nu existÄ articole în <strong>${catLabel(category)}</strong>.` : 'Nu existÄ articole.'}</p> 258 </div>`; 259 return; 260 } 261 el.innerHTML = `<div class="articles-grid ${gridClass}">` + 262 filtered.map(renderCard).join('') + '</div>'; 263 } catch (e) { 264 el.innerHTML = `<div class="empty-state"><p>Eroare la încÄrcarea articolelor.</p></div>`; 265 console.error(e); 266 } 267} 268 269// ââ Load Breaking News Ticker âââââââââââââââââââââââââââ 270async function loadTicker() { 271 const el = document.querySelector('.ticker-content'); 272 if (!el) return; 273 try { 274 const arts = await fetchAllArticles(); 275 if (!arts.length) return; 276 const items = arts.slice(0, 6).map(a => 277 `<span class="ticker-item" onclick="location.href='/${(a.category || 'altele').toLowerCase()}/${a.slug || a.id}'">${a.title}</span>` 278 ); 279 el.innerHTML = items.join('') + items.join(''); 280 } catch (e) { console.error(e); } 281} 282 283// ââ Load Sidebar Recent âââââââââââââââââââââââââââââââââ 284async function loadSidebarRecent() { 285 const el = document.getElementById('sidebar-recent'); 286 if (!el) return; 287 try { 288 const arts = await fetchAllArticles(); 289 el.innerHTML = arts.slice(0, 7).map(renderListItem).join('') || 290 '<div style="padding:16px;color:#999;font-size:13px">Nu existÄ articole recente.</div>'; 291 } catch (e) { console.error(e); } 292} 293 294// ââ Load Related Articles âââââââââââââââââââââââââââââââ 295async function loadRelated(category, excludeId) { 296 const el = document.getElementById('related-articles'); 297 if (!el) return; 298 try { 299 const arts = await fetchAllArticles(); 300 const related = arts.filter(a => a.category === category && a.id !== excludeId).slice(0, 4); 301 if (!related.length) { el.parentElement && (el.parentElement.style.display = 'none'); return; } 302 el.innerHTML = `<div class="articles-grid col-3">` + related.map(renderCard).join('') + '</div>'; 303 } catch (e) { console.error(e); } 304} 305 306// ââ Load Single Article (by slug OR by id) ââââââââââââââ 307async function loadArticle(slugOrId) { 308 const heroEl = document.getElementById('article-hero'); 309 const metaEl = document.getElementById('article-meta'); 310 const contentEl = document.getElementById('article-content'); 311 if (!contentEl) return; 312 313 try { 314 // Detect: if it looks like a Firestore ID (20 chars, no hyphens except Firestore IDs) 315 // We try slug first, fallback to ID 316 let art; 317 const looksLikeId = /^[A-Za-z0-9]{15,}$/.test(slugOrId); 318 if (looksLikeId) { 319 try { art = await fetchArticle(slugOrId); } 320 catch { art = await fetchArticleBySlug(slugOrId); } 321 } else { 322 try { art = await fetchArticleBySlug(slugOrId); } 323 catch { art = await fetchArticle(slugOrId); } 324 } 325 326 document.title = `${art.title} â SportFan.ro`; 327 const canonical = document.getElementById('canonical-url'); 328 const cat = art.category ? art.category.toLowerCase() : 'altele'; 329 const urlCat = cat === 'stiri_actuale' ? 'stiri-actuale' : cat; 330 if (canonical) canonical.href = `/${urlCat}/${art.slug || art.id}`; 331 332 const image = art.image || `https://picsum.photos/seed/${art.id}/1200/600`; 333 334 if (heroEl) { 335 heroEl.innerHTML = ` 336 <div class="article-hero"> 337 <img src="${image}" alt="${art.title}" 338 onerror="this.src='https://picsum.photos/seed/art/1200/600'"> 339 <div class="article-hero-overlay">
340 <span class="badge cat-badge-${cat}">${catLabel(cat)}</span> 341 <h1>${art.title}</h1> 342 </div> 343 </div>`; 344 } 345 if (metaEl) { 346 metaEl.innerHTML = ` 347 <span>âï¸ ${art.author || 'RedacÈie SportFan.ro'}</span> 348 <span>ð ${formatDate(art.createdAt)}</span> 349 <span>ðï¸ ${(art.views || 0) + 1} vizualizÄri</span> 350 <span>ð·ï¸ <a href="/${cat}" class="cat-${cat}">${catLabel(cat)}</a></span>`; 351 } 352 let rawContent = art.content || '<p>ConÈinut indisponibil.</p>'; 353 354 355 356 contentEl.innerHTML = rawContent; 357 const bcCat = document.getElementById('breadcrumb-cat'); 358 const bcTitle = document.getElementById('breadcrumb-title'); 359 if (bcCat) bcCat.innerHTML = `<a href="/${cat}">${catLabel(cat)}</a>`; 360 if (bcTitle) bcTitle.textContent = art.title; 361 362 incrementViews(art.id); 363 loadRelated(cat, art.id); 364 loadSidebarRecent(); 365 366 return art; 367 } catch (e) { 368 if (contentEl) contentEl.innerHTML = '<p style="color:red">Articolul nu a fost gÄsit sau a apÄrut o eroare.</p>'; 369 console.error(e); 370 throw e; 371 } 372} 373 374// ââ Lazy Load (IntersectionObserver) ââââââââââââââââââââ 375// Only loads a section when it's close to the viewport 376function lazyLoad(targetId, loaderFn) { 377 const el = document.getElementById(targetId); 378 if (!el) return; 379 if (!('IntersectionObserver' in window)) { loaderFn(); return; } 380 const obs = new IntersectionObserver((entries, o) => { 381 entries.forEach(entry => { 382 if (entry.isIntersecting) { 383 o.unobserve(entry.target); 384 loaderFn(); 385 } 386 }); 387 }, { rootMargin: '200px' }); // start loading 200px before visible 388 obs.observe(el); 389} 390 391 392// ââ Active Nav ââââââââââââââââââââââââââââââââââââââââââ 393function setActiveNav() { 394 const page = location.pathname.split('/').pop() || 'index.html'; 395 document.querySelectorAll('.main-nav a').forEach(a => { 396 a.classList.remove('active'); 397 const href = a.getAttribute('href') || ''; 398 if (href === page || (page === '' && href === 'index.html')) a.classList.add('active'); 399 }); 400} 401 402// ââ Back to Top âââââââââââââââââââââââââââââââââââââââââ 403function initBackToTop() { 404 const btn = document.querySelector('.back-to-top'); 405 if (!btn) return; 406 window.addEventListener('scroll', () => btn.classList.toggle('visible', window.scrollY > 400)); 407 btn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' })); 408} 409 410// ââ Init ââââââââââââââââââââââââââââââââââââââââââââââââ 411document.addEventListener('DOMContentLoaded', () => { 412 setActiveNav(); 413 initBackToTop(); 414 loadTicker(); 415 initLiveVisitors(); 416}); 417 418// ââ Live Visitors Tracker âââââââââââââââââââââââââââââââ 419function initLiveVisitors() { 420 if (typeof rtdb === 'undefined' || !rtdb) return; 421 422 // Check if we are on a public page with the counter element 423 const counterEl = document.getElementById('live-visitors-count'); 424 if (!counterEl) return; 425 426 // Generate a unique session ID for the current tab 427 const sessionId = Math.random().toString(36).substr(2, 9); 428 429 // References 430 const presenceRef = rtdb.ref('presence/' + sessionId); 431 const listRef = rtdb.ref('presence'); 432 const connectedRef = rtdb.ref('.info/connected'); 433 434 // Manage Connection State 435 connectedRef.on('value', (snap) => { 436 if (snap.val() === true) { 437 // Remove ourselves when we disconnect 438 presenceRef.onDisconnect().remove(); 439 440 // Set our presence status 441 presenceRef.set({ 442 timestamp: firebase.database.ServerValue.TIMESTAMP 443 }); 444 } 445 });
446 447 // Listen for changes in the total number of connected users 448 listRef.on('value', (snap) => { 449 const count = snap.numChildren(); 450 counterEl.textContent = count > 0 ? count : 1; 451 }); 452}
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.