Spaces:
Running
Running
| let allNews = []; | |
| let sentimentTrend = []; | |
| let currentFilter = 'all'; | |
| let currentSource = 'all'; | |
| let searchQuery = ''; | |
| let renderedCount = 0; | |
| let currentRenderedFiltered = []; | |
| const IMPACT_KEYWORDS = { | |
| high: ['crash', 'crisis', 'urgent', 'breaking', 'collapsed', 'warns', 'surge', 'plunge', 'lockdown', 'rating', 'result', 'focus'], | |
| pos: ['profit', 'growth', 'record', 'dividend', 'buyback', 'partnership', 'acquired', 'expansion', 'jump', 'bullish', 'buy'], | |
| neg: ['loss', 'fall', 'slump', 'down', 'decline', 'investigation', 'scam', 'penalty', 'lawsuit', 'bearish', 'sell'] | |
| }; | |
| const IMPACT_REGEX = Object.fromEntries( | |
| Object.entries(IMPACT_KEYWORDS).map(([type, words]) => [ type, new RegExp(`\\b(${words.join('|')})\\b`, 'gi') ]) | |
| ); | |
| async function fetchNews() { | |
| try { | |
| const response = await fetch('/api/news'); | |
| const data = await response.json(); | |
| allNews = data.news; | |
| sentimentTrend = data.trend; | |
| renderDashboard(); | |
| renderIndices(data.indices); | |
| } catch (error) { | |
| console.error('Error fetching news:', error); | |
| const feed = document.getElementById('news-feed'); | |
| if (feed) feed.innerHTML = '<div class="loader">Market data sync failed. Check connection.</div>'; | |
| } | |
| } | |
| async function fetchIndicesOnly() { | |
| try { | |
| const response = await fetch('/api/indices'); | |
| const data = await response.json(); | |
| renderIndices(data.indices); | |
| } catch (error) { | |
| console.error('Error updating indices:', error); | |
| } | |
| } | |
| function renderDashboard() { | |
| renderFeed(); | |
| } | |
| function renderFeed() { | |
| const feed = document.getElementById('news-feed'); | |
| if (!feed) return; | |
| let filtered = allNews; | |
| if (currentFilter !== 'all') filtered = filtered.filter(n => n.sentiment.label === currentFilter); | |
| if (currentSource !== 'all') filtered = filtered.filter(n => n.source === currentSource); | |
| if (searchQuery) { | |
| const query = searchQuery.toLowerCase(); | |
| filtered = filtered.filter(n => | |
| n.title.toLowerCase().includes(query) || | |
| n.source.toLowerCase().includes(query) | |
| ); | |
| } | |
| // Filter out headlines with less than 5 words | |
| filtered = filtered.filter(n => { | |
| const words = n.title.trim().split(/\s+/).filter(w => /[a-zA-Z0-9]/.test(w)); | |
| return words.length >= 5; | |
| }); | |
| currentRenderedFiltered = filtered; | |
| renderedCount = 0; | |
| if (filtered.length === 0) { | |
| feed.innerHTML = '<div class="loader">No matches for current trading filters.</div>'; | |
| return; | |
| } | |
| feed.innerHTML = ''; | |
| renderMoreItems(); | |
| } | |
| function renderMoreItems() { | |
| const feed = document.getElementById('news-feed'); | |
| if (!feed) return; | |
| const oldTrigger = document.getElementById('load-more-trigger'); | |
| if (oldTrigger) oldTrigger.remove(); | |
| const nextCount = Math.min(renderedCount + 100, currentRenderedFiltered.length); | |
| const itemsToRender = currentRenderedFiltered.slice(renderedCount, nextCount); | |
| const html = itemsToRender.map((item, index) => { | |
| const timeAgo = getTimeAgo(new Date(item.pubDate)); | |
| const highlightedTitle = highlightImpact(item.title); | |
| return ` | |
| <article class="news-article" style="animation-delay: ${index * 0.02}s"> | |
| <div class="article-header"> | |
| <span class="source">${item.source}</span> | |
| <span class="time">${timeAgo}</span> | |
| </div> | |
| <a href="${item.link}" target="_blank" style="text-decoration: none; color: inherit;"> | |
| <h3>${highlightedTitle}</h3> | |
| </a> | |
| </article> | |
| `; | |
| }).join(''); | |
| feed.insertAdjacentHTML('beforeend', html); | |
| renderedCount = nextCount; | |
| if (renderedCount < currentRenderedFiltered.length) { | |
| const trigger = document.createElement('div'); | |
| trigger.id = 'load-more-trigger'; | |
| trigger.className = 'loader'; | |
| trigger.textContent = 'Scroll for more...'; | |
| feed.appendChild(trigger); | |
| setupObserver(trigger); | |
| } | |
| } | |
| function setupObserver(trigger) { | |
| const observer = new IntersectionObserver((entries) => { | |
| if (entries[0].isIntersecting) { | |
| observer.disconnect(); | |
| renderMoreItems(); | |
| } | |
| }); | |
| observer.observe(trigger); | |
| } | |
| function renderIndices(indices) { | |
| const container = document.getElementById('index-ticker'); | |
| if (!container || !indices) return; | |
| container.innerHTML = Object.entries(indices).map(([name, data]) => { | |
| const isUp = parseFloat(data.change) >= 0; | |
| const sign = isUp ? '+' : ''; | |
| return ` | |
| <div class="ticker-item"> | |
| <span class="ticker-name">${name}</span> | |
| <div class="ticker-data"> | |
| <span class="ticker-price">${parseFloat(data.price).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</span> | |
| <span class="ticker-change ${isUp ? 'up' : 'down'}">${sign}${data.changePercent}%</span> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| function highlightImpact(title) { | |
| let result = title; | |
| Object.entries(IMPACT_REGEX).forEach(([type, regex]) => { | |
| result = result.replace(regex, `<span class="impact-word impact-${type}">$1</span>`); | |
| }); | |
| return result; | |
| } | |
| function getTimeAgo(date) { | |
| const seconds = Math.floor((new Date() - date) / 1000); | |
| let interval = seconds / 3600; | |
| if (interval > 1) return Math.floor(interval) + 'h ago'; | |
| interval = seconds / 60; | |
| if (interval > 1) return Math.floor(interval) + 'm ago'; | |
| return 'Just now'; | |
| } | |
| function filterBySentiment(sentiment) { | |
| currentFilter = sentiment; | |
| document.querySelectorAll('.filter-btn').forEach(btn => { | |
| btn.classList.toggle('active', btn.dataset.sentiment === sentiment); | |
| }); | |
| renderFeed(); | |
| } | |
| let searchTimeout; | |
| function handleSearch(val) { | |
| clearTimeout(searchTimeout); | |
| searchTimeout = setTimeout(() => { | |
| searchQuery = val; | |
| renderFeed(); | |
| }, 300); | |
| } | |
| function openSupportModal() { | |
| document.getElementById('support-modal').classList.add('active'); | |
| history.pushState({ modal: 'support' }, ''); | |
| } | |
| function closeSupportModal() { | |
| document.getElementById('support-modal').classList.remove('active'); | |
| } | |
| window.addEventListener('popstate', function (e) { | |
| if (document.getElementById('support-modal').classList.contains('active')) { | |
| closeSupportModal(); | |
| } | |
| }); | |
| fetchNews(); | |
| setInterval(fetchNews, 30000); | |
| setInterval(fetchIndicesOnly, 10000); | |