Spaces:
Running
Running
Fix: World Cup section + EPG timezone + CSS
Browse files- static/app_v2.js +134 -14
static/app_v2.js
CHANGED
|
@@ -29,11 +29,12 @@ async function loadHome(){
|
|
| 29 |
loadHotTopics();
|
| 30 |
|
| 31 |
// Fetch all data with timeout - không để 1 API chậm treo cả trang
|
| 32 |
-
const [featuredData, wallData, hlLeagues, aiData] = await Promise.allSettled([
|
| 33 |
_fetchWithTimeout('/api/livescore/featured', 5000),
|
| 34 |
_fetchWithTimeout('/api/wall', 5000),
|
| 35 |
-
_fetchWithTimeout('/api/highlights/leagues', 15000),
|
| 36 |
_fetchWithTimeout('/api/genk_ai', 8000),
|
|
|
|
| 37 |
]).then(results => results.map(r => r.status === 'fulfilled' ? r.value : null));
|
| 38 |
|
| 39 |
if(featuredData && featuredData.home){
|
|
@@ -44,29 +45,148 @@ async function loadHome(){
|
|
| 44 |
}
|
| 45 |
|
| 46 |
_wallPosts = (wallData && wallData.posts) || [];
|
| 47 |
-
// Đảm bảo _hlLeagueData luôn là object, không bao giờ null
|
| 48 |
_hlLeagueData = (hlLeagues && typeof hlLeagues === 'object') ? hlLeagues : {};
|
|
|
|
| 49 |
|
| 50 |
_renderWallIn(afterEl);
|
| 51 |
_renderHLIn(afterEl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
if(aiData && aiData.length) _renderSlidesIn('ai-articles','Ứng dụng AI','🤖',aiData,afterEl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
}
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
try{
|
| 63 |
-
const
|
| 64 |
-
if(
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
}
|
| 68 |
}catch(e){
|
| 69 |
-
console.log(
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
|
|
|
| 29 |
loadHotTopics();
|
| 30 |
|
| 31 |
// Fetch all data with timeout - không để 1 API chậm treo cả trang
|
| 32 |
+
const [featuredData, wallData, hlLeagues, aiData, wcData] = await Promise.allSettled([
|
| 33 |
_fetchWithTimeout('/api/livescore/featured', 5000),
|
| 34 |
_fetchWithTimeout('/api/wall', 5000),
|
| 35 |
+
_fetchWithTimeout('/api/highlights/leagues', 15000),
|
| 36 |
_fetchWithTimeout('/api/genk_ai', 8000),
|
| 37 |
+
_fetchWithTimeout('/api/wc2026', 12000), // World Cup data
|
| 38 |
]).then(results => results.map(r => r.status === 'fulfilled' ? r.value : null));
|
| 39 |
|
| 40 |
if(featuredData && featuredData.home){
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
_wallPosts = (wallData && wallData.posts) || [];
|
|
|
|
| 48 |
_hlLeagueData = (hlLeagues && typeof hlLeagues === 'object') ? hlLeagues : {};
|
| 49 |
+
_wcData = wcData || {}; // Store World Cup data globally
|
| 50 |
|
| 51 |
_renderWallIn(afterEl);
|
| 52 |
_renderHLIn(afterEl);
|
| 53 |
+
|
| 54 |
+
// Render World Cup section nếu có dữ liệu
|
| 55 |
+
if(_wcData && Object.keys(_wcData).length > 0){
|
| 56 |
+
_renderWorldCupSection(afterEl);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
if(aiData && aiData.length) _renderSlidesIn('ai-articles','Ứng dụng AI','🤖',aiData,afterEl);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// ===== WORLD CUP 2026 SECTION =====
|
| 63 |
+
function _renderWorldCupSection(afterEl){
|
| 64 |
+
const wc = _wcData;
|
| 65 |
+
const wrap = document.createElement('div');
|
| 66 |
+
wrap.className = 'wc-section';
|
| 67 |
+
wrap.id = 'wc-section';
|
| 68 |
|
| 69 |
+
let h = '<div class="wc-header"><span class="wc-title">🌍 World Cup 2026</span></div>';
|
| 70 |
+
h += '<div class="wc-tabs"><span class="wc-tab active" onclick="switchWCTab(\'news\',this)">📰 Tin tức</span><span class="wc-tab" onclick="switchWCTab(\'fixtures\',this)">📅 Lịch thi đấu</span><span class="wc-tab" onclick="switchWCTab(\'standings\',this)">🏆 Bảng xếp hạng</span><span class="wc-tab" onclick="switchWCTab(\'stats\',this)">📊 Thống kê</span><span class="wc-tab" onclick="switchWCTab(\'highlights\',this)">🎬 Highlight</span></div>';
|
| 71 |
+
h += '<div id="wc-tab-content" class="wc-tab-content"></div>';
|
| 72 |
+
|
| 73 |
+
wrap.innerHTML = h;
|
| 74 |
+
afterEl.parentNode.insertBefore(wrap, afterEl);
|
| 75 |
+
|
| 76 |
+
// Mặc định hiển thị tin tức
|
| 77 |
+
_renderWCTabContent('news');
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function switchWCTab(tab, el){
|
| 81 |
+
document.querySelectorAll('.wc-tab').forEach(t => t.classList.remove('active'));
|
| 82 |
+
if(el) el.classList.add('active');
|
| 83 |
+
_renderWCTabContent(tab);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function _renderWCTabContent(tab){
|
| 87 |
+
const contentEl = document.getElementById('wc-tab-content');
|
| 88 |
+
if(!contentEl) return;
|
| 89 |
+
|
| 90 |
+
if(tab === 'news'){
|
| 91 |
+
_renderWCNews(contentEl);
|
| 92 |
+
} else if(tab === 'fixtures'){
|
| 93 |
+
_renderWCFixtures(contentEl);
|
| 94 |
+
} else if(tab === 'standings'){
|
| 95 |
+
_renderWCStandings(contentEl);
|
| 96 |
+
} else if(tab === 'stats'){
|
| 97 |
+
_renderWCStats(contentEl);
|
| 98 |
+
} else if(tab === 'highlights'){
|
| 99 |
+
_renderWCHighlights(contentEl);
|
| 100 |
}
|
| 101 |
}
|
| 102 |
|
| 103 |
+
function _renderWCNews(el){
|
| 104 |
+
const news = _wcData.news || [];
|
| 105 |
+
if(!news.length){
|
| 106 |
+
el.innerHTML = '<div class="loading">Đang tải tin tức...</div>';
|
| 107 |
+
// Fetch news nếu chưa có
|
| 108 |
+
_fetchWCTabData('news');
|
| 109 |
+
return;
|
| 110 |
+
}
|
| 111 |
+
let h = '<div class="wc-news-list">';
|
| 112 |
+
news.slice(0, 15).forEach(item => {
|
| 113 |
+
h += `<div class="wc-news-item" onclick="readArticle('${esc(item.link)}')">`;
|
| 114 |
+
if(item.img) h += `<div class="wc-news-img"><img src="${esc(item.img)}" loading="lazy" onerror="this.style.display='none'"></div>`;
|
| 115 |
+
h += `<div class="wc-news-text"><div class="wc-news-title">${esc(item.title)}</div><div class="wc-news-source">${esc(item.source||'')}</div></div></div>`;
|
| 116 |
+
});
|
| 117 |
+
h += '</div>';
|
| 118 |
+
el.innerHTML = h;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function _renderWCFixtures(el){
|
| 122 |
+
const fixtures = _wcData.fixtures || {};
|
| 123 |
+
const matches = fixtures.matches || [];
|
| 124 |
+
if(!matches.length){
|
| 125 |
+
el.innerHTML = '<div class="loading">Đang tải lịch thi đấu...</div>';
|
| 126 |
+
_fetchWCTabData('fixtures');
|
| 127 |
+
return;
|
| 128 |
+
}
|
| 129 |
+
let h = '<div class="wc-fixtures-list">';
|
| 130 |
+
matches.slice(0, 20).forEach(m => {
|
| 131 |
+
const statusClass = m.status === 'live' ? 'wc-live' : (m.status === 'finished' ? 'wc-finished' : 'wc-upcoming');
|
| 132 |
+
const score = m.score || 'vs';
|
| 133 |
+
h += `<div class="wc-match-item">`;
|
| 134 |
+
h += `<div class="wc-match-date">${esc(m.date_vn||m.date_utc||'')}</div>`;
|
| 135 |
+
h += `<div class="wc-match-teams"><span class="wc-team">${esc(m.home||'')}</span><span class="wc-score ${statusClass}">${esc(score)}</span><span class="wc-team">${esc(m.away||'')}</span></div>`;
|
| 136 |
+
if(m.location) h += `<div class="wc-match-location">📍 ${esc(m.location)}</div>`;
|
| 137 |
+
h += `</div>`;
|
| 138 |
+
});
|
| 139 |
+
h += '</div>';
|
| 140 |
+
el.innerHTML = h;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
function _renderWCStandings(el){
|
| 144 |
+
const standings = _wcData.standings || {};
|
| 145 |
+
const html = standings.html || '';
|
| 146 |
+
if(!html){
|
| 147 |
+
el.innerHTML = '<div class="loading">Đang tải bảng xếp hạng...</div>';
|
| 148 |
+
_fetchWCTabData('standings');
|
| 149 |
+
return;
|
| 150 |
+
}
|
| 151 |
+
el.innerHTML = '<div class="wc-standings-table">' + html + '</div>';
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
function _renderWCStats(el){
|
| 155 |
+
const stats = _wcData.stats || {};
|
| 156 |
+
const html = stats.html || '';
|
| 157 |
+
if(!html){
|
| 158 |
+
el.innerHTML = '<div class="loading">Đang tải thống kê...</div>';
|
| 159 |
+
_fetchWCTabData('stats');
|
| 160 |
+
return;
|
| 161 |
+
}
|
| 162 |
+
el.innerHTML = '<div class="wc-stats-table">' + html + '</div>';
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
function _renderWCHighlights(el){
|
| 166 |
+
const highlights = _hlLeagueData['world-cup'] || [];
|
| 167 |
+
if(!highlights.length){
|
| 168 |
+
el.innerHTML = '<div class="loading">Đang tải highlights...</div>';
|
| 169 |
+
_fetchWCTabData('highlights');
|
| 170 |
+
return;
|
| 171 |
+
}
|
| 172 |
+
let h = '<div class="wc-highlights-grid">';
|
| 173 |
+
highlights.slice(0, 12).forEach((item, i) => {
|
| 174 |
+
h += `<div class="wc-hl-item" onclick="openHighlightFeed('world-cup',${i})">`;
|
| 175 |
+
h += `<div class="wc-hl-thumb">${item.img ? `<img src="${esc(item.img)}" loading="lazy">` : ''}<div class="card-play">▶</div></div>`;
|
| 176 |
+
h += `<div class="wc-hl-title">${esc(item.title)}</div></div>`;
|
| 177 |
+
});
|
| 178 |
+
h += '</div>';
|
| 179 |
+
el.innerHTML = h;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
async function _fetchWCTabData(tab){
|
| 183 |
try{
|
| 184 |
+
const data = await _fetchWithTimeout(`/api/wc2026/${tab}`, 10000);
|
| 185 |
+
if(!_wcData) _wcData = {};
|
| 186 |
+
_wcData[tab] = data;
|
| 187 |
+
_renderWCTabContent(tab);
|
|
|
|
| 188 |
}catch(e){
|
| 189 |
+
console.log(`WC ${tab} fetch failed:`, e.message);
|
| 190 |
}
|
| 191 |
}
|
| 192 |
|