Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>News Feed Hub</title> | |
<style> | |
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; color: #333; } | |
h1 { text-align: center; color: #2c3e50; } | |
.search-container { text-align: center; margin: 20px 0; } | |
.search-bar { width: 50%; padding: 10px; border: 2px solid #3498db; border-radius: 25px; margin-right: 10px; } | |
.search-button { padding: 10px 20px; border: none; border-radius: 25px; background-color: #3498db; color: white; cursor: pointer; font-size: 1em; } | |
.search-button:hover { background-color: #2980b9; } | |
.category-section { margin: 20px 0; } | |
.category-title { background-color: #3498db; color: white; padding: 10px; border-radius: 5px; cursor: pointer; } | |
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } | |
.article-tile { background: white; height: 350px; overflow-y: clip; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } | |
.article-tile img, .article-tile svg { width: 100%; height: 150px; object-fit: cover; border-radius: 5px; } | |
.title a { font-size: 1.3em; color: #2c3e50; text-decoration: none; font-weight: 600; text-transform: capitalize; } | |
.title a:hover { color: #3498db; } | |
.description { color: #555; font-size: 0.9em; height: 150px; overflow-y: clip; } | |
.published { font-size: 0.8em; color: #95a5a6; } | |
.no-articles { text-align: center; color: #2c3e50; margin-top: 20px; } | |
.loading-container { text-align: center; margin: 10px 0; } | |
.loading-spinner { display: inline-block; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; } | |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
</style> | |
</head> | |
<body> | |
<h1>News Feed Hub</h1> | |
<div class="search-container"> | |
<form method="POST" action="/search" id="searchForm"> | |
<input type="text" name="search" class="search-bar" placeholder="Search news..."> | |
<button type="submit" class="search-button">Search</button> | |
</form> | |
<button id="backButton" style="display: none; margin-top: 10px;">Back to Main</button> | |
</div> | |
{% if loading %} | |
<div class="loading-container" id="loadingContainer"> | |
<span class="loading-spinner"></span> | |
</div> | |
{% endif %} | |
{% if has_articles %} | |
<div id="articlesContainer"> | |
{% for category, articles in categorized_articles.items() %} | |
<div class="category-section"> | |
<div class="category-title" onclick="toggleCategory('{{ category }}')">{{ category }} <span class="loading-spinner" id="spinner-{{ category }}" style="display: none;"></span></div> | |
<div class="tiles" id="category-{{ category }}" data-last-update="0"> | |
{% set seen_articles = [] %} | |
{% for article in articles %} | |
{% set article_key = article.title + '|' + article.link + '|' + article.published %} | |
{% if article_key not in seen_articles %} | |
<div class="article-tile" data-published="{{ article.published }}" data-id="{{ loop.index }}" data-key="{{ article_key }}"> | |
{% if article.image != "svg" %} | |
<img src="{{ article.image }}" alt="Article Image"> | |
{% else %} | |
<svg width="100%" height="150" xmlns="http://www.w3.org/2000/svg"> | |
<rect width="100%" height="100%" fill="#e0e0e0"/> | |
<text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#666">No Image</text> | |
</svg> | |
{% endif %} | |
<div class="title"><a href="{{ article.link }}" target="_blank">{{ article.title }}</a></div> | |
<div class="description">{{ article.description }}</div> | |
<div class="published">Published: {{ article.published }}</div> | |
</div> | |
{% set seen_articles = seen_articles + [article_key] %} | |
{% endif %} | |
{% endfor %} | |
</div> | |
<div class="tiles" id="all-{{ category }}" style="display: none;"></div> | |
</div> | |
{% endfor %} | |
</div> | |
{% else %} | |
{% if not loading %} | |
<div class="no-articles">No articles available yet. Loading new feeds...</div> | |
{% endif %} | |
{% endif %} | |
<script> | |
let lastUpdate = 0; | |
let expandedCategories = new Set(); | |
function getArticleKey(article) { | |
return `${article.title}|${article.link}|${article.published}`; | |
} | |
function toggleCategory(category) { | |
const spinner = document.getElementById(`spinner-${category}`); | |
const tilesDiv = document.getElementById(`category-${category}`); | |
const allTilesDiv = document.getElementById(`all-${category}`); | |
if (expandedCategories.has(category)) { | |
tilesDiv.style.display = 'grid'; | |
allTilesDiv.style.display = 'none'; | |
expandedCategories.delete(category); | |
} else { | |
spinner.style.display = 'inline-block'; | |
fetch(`/get_all_articles/${category}`) | |
.then(response => response.json()) | |
.then(data => { | |
spinner.style.display = 'none'; | |
if (data.articles.length > 0) { | |
let html = ''; | |
const seenKeys = new Set(); | |
data.articles.sort((a, b) => new Date(b.published) - new Date(a.published)); | |
data.articles.forEach((article, index) => { | |
const key = getArticleKey(article); | |
if (!seenKeys.has(key)) { | |
seenKeys.add(key); | |
html += ` | |
<div class="article-tile" data-published="${article.published}" data-id="${index}" data-key="${key}"> | |
${article.image !== "svg" ? `<img src="${article.image}" alt="Article Image">` : ` | |
<svg width="100%" height="150" xmlns="http://www.w3.org/2000/svg"> | |
<rect width="100%" height="100%" fill="#e0e0e0"/> | |
<text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#666">No Image</text> | |
</svg> | |
`} | |
<div class="title"><a href="${article.link}" target="_blank">${article.title}</a></div> | |
<div class="description">${article.description}</div> | |
<div class="published">Published: ${article.published}</div> | |
</div> | |
`; | |
} else { | |
console.warn(`Duplicate article skipped in category ${category}: ${key}`); | |
} | |
}); | |
tilesDiv.style.display = 'none'; | |
allTilesDiv.innerHTML = html; | |
allTilesDiv.style.display = 'grid'; | |
expandedCategories.add(category); | |
} else { | |
alert(`No additional articles found for ${category}.`); | |
} | |
}) | |
.catch(error => { | |
spinner.style.display = 'none'; | |
console.error(`Error loading all articles for ${category}:`, error); | |
alert(`Failed to load all articles for ${category}. Please try again.`); | |
}); | |
} | |
} | |
function updateArticles() { | |
fetch('/get_updates') | |
.then(response => response.json()) | |
.then(data => { | |
if (data.articles && data.last_update > lastUpdate) { | |
lastUpdate = data.last_update; | |
const newArticles = data.articles; | |
for (const [category, articles] of Object.entries(newArticles)) { | |
const tilesDiv = document.getElementById(`category-${category}`); | |
const allTilesDiv = document.getElementById(`all-${category}`); | |
if (tilesDiv) { | |
const existingArticles = Array.from(tilesDiv.querySelectorAll('.article-tile')); | |
let currentIds = new Set(existingArticles.map(a => a.dataset.id)); | |
let currentKeys = new Set(existingArticles.map(a => a.dataset.key)); | |
let newHtml = ''; | |
articles.sort((a, b) => new Date(b.published) - new Date(a.published)); | |
articles.slice(0, 10).forEach((article, index) => { | |
const key = getArticleKey(article); | |
if (!currentKeys.has(key)) { | |
newHtml += ` | |
<div class="article-tile" data-published="${article.published}" data-id="${index}" data-key="${key}"> | |
${article.image !== "svg" ? `<img src="${article.image}" alt="Article Image">` : ` | |
<svg width="100%" height="150" xmlns="http://www.w3.org/2000/svg"> | |
<rect width="100%" height="100%" fill="#e0e0e0"/> | |
<text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#666">No Image</text> | |
</svg> | |
`} | |
<div class="title"><a href="${article.link}" target="_blank">${article.title}</a></div> | |
<div class="description">${article.description}</div> | |
<div class="published">Published: ${article.published}</div> | |
</div> | |
`; | |
} else { | |
console.warn(`Duplicate article skipped in update for ${category}: ${key}`); | |
} | |
}); | |
if (newHtml) { | |
tilesDiv.innerHTML = (existingArticles.map(a => a.outerHTML).join('') + newHtml); | |
const allLimited = Array.from(tilesDiv.querySelectorAll('.article-tile')); | |
tilesDiv.innerHTML = allLimited.sort((a, b) => new Date(b.dataset.published) - new Date(a.dataset.published)).slice(0, 10).map(a => a.outerHTML).join(''); | |
} | |
if (allTilesDiv.style.display === 'grid') { | |
let allNewHtml = ''; | |
const allSeenKeys = new Set(Array.from(allTilesDiv.querySelectorAll('.article-tile')).map(a => a.dataset.key)); | |
articles.forEach((article, index) => { | |
const key = getArticleKey(article); | |
if (!allSeenKeys.has(key)) { | |
allSeenKeys.add(key); | |
allNewHtml += ` | |
<div class="article-tile" data-published="${article.published}" data-id="${index}" data-key="${key}"> | |
${article.image !== "svg" ? `<img src="${article.image}" alt="Article Image">` : ` | |
<svg width="100%" height="150" xmlns="http://www.w3.org/2000/svg"> | |
<rect width="100%" height="100%" fill="#e0e0e0"/> | |
<text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#666">No Image</text> | |
</svg> | |
`} | |
<div class="title"><a href="${article.link}" target="_blank">${article.title}</a></div> | |
<div class="description">${article.description}</div> | |
<div class="published">Published: ${article.published}</div> | |
</div> | |
`; | |
} else { | |
console.warn(`Duplicate article skipped in expanded view for ${category}: ${key}`); | |
} | |
}); | |
allTilesDiv.innerHTML = (Array.from(allTilesDiv.querySelectorAll('.article-tile')).map(a => a.outerHTML).join('') + allNewHtml); | |
const allExpanded = Array.from(allTilesDiv.querySelectorAll('.article-tile')); | |
allTilesDiv.innerHTML = allExpanded.sort((a, b) => new Date(b.dataset.published) - new Date(a.dataset.published)).map(a => a.outerHTML).join(''); | |
} | |
} | |
} | |
} | |
setTimeout(updateArticles, 2000); | |
}) | |
.catch(error => { | |
console.error('Error updating articles:', error); | |
setTimeout(updateArticles, 2000); | |
}); | |
} | |
function checkLoadingStatus() { | |
fetch('/check_loading') | |
.then(response => response.json()) | |
.then(data => { | |
if (data.status === 'complete') { | |
window.location.reload(); // Refresh the page when loading is complete | |
} else { | |
setTimeout(checkLoadingStatus, 2000); // Check again after 2 seconds | |
} | |
}) | |
.catch(error => { | |
console.error('Error checking loading status:', error); | |
setTimeout(checkLoadingStatus, 2000); | |
}); | |
} | |
document.addEventListener('DOMContentLoaded', () => { | |
const tiles = document.querySelectorAll('.tiles'); | |
tiles.forEach(tile => { | |
lastUpdate = Math.max(lastUpdate, parseFloat(tile.dataset.lastUpdate) || 0); | |
}); | |
updateArticles(); | |
// Start checking loading status if loading is active | |
if (document.getElementById('loadingContainer')) { | |
checkLoadingStatus(); | |
} | |
const form = document.getElementById('searchForm'); | |
const backButton = document.getElementById('backButton'); | |
form.addEventListener('submit', (event) => { | |
event.preventDefault(); | |
fetch('/search', { | |
method: 'POST', | |
body: new FormData(form) | |
}) | |
.then(response => { | |
if (!response.ok) throw new Error('Network response was not ok'); | |
return response.json(); | |
}) | |
.then(data => { | |
if (data.has_articles) { | |
backButton.style.display = 'block'; | |
const existingContent = document.querySelectorAll('.category-section'); | |
existingContent.forEach(section => section.remove()); | |
const categoriesHtml = Object.entries(data.categorized_articles).map(([cat, articles]) => { | |
const seenKeys = new Set(); | |
const uniqueArticles = articles.filter(article => { | |
const key = getArticleKey(article); | |
if (seenKeys.has(key)) { | |
console.warn(`Duplicate article skipped in search for ${cat}: ${key}`); | |
return false; | |
} | |
seenKeys.add(key); | |
return true; | |
}); | |
return ` | |
<div class="category-section"> | |
<div class="category-title" onclick="toggleCategory('${cat}')">${cat} <span class="loading-spinner" id="spinner-${cat}" style="display: none;"></span></div> | |
<div class="tiles" id="category-${cat}" data-last-update="0"> | |
${uniqueArticles.map((article, index) => ` | |
<div class="article-tile" data-published="${article.published}" data-id="${index}" data-key="${getArticleKey(article)}"> | |
${article.image !== "svg" ? `<img src="${article.image}" alt="Article Image">` : ` | |
<svg width="100%" height="150" xmlns="http://www.w3.org/2000/svg"> | |
<rect width="100%" height="100%" fill="#e0e0e0"/> | |
<text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#666">No Image</text> | |
</svg> | |
`} | |
<div class="title"><a href="${article.link}" target="_blank">${article.title}</a></div> | |
<div class="description">${article.description}</div> | |
<div class="published">Published: ${article.published}</div> | |
</div> | |
`).join('')} | |
</div> | |
<div class="tiles" id="all-${cat}" style="display: none;"></div> | |
</div> | |
`; | |
}).join(''); | |
document.querySelector('.search-container').insertAdjacentHTML('afterend', categoriesHtml); | |
} else { | |
alert('No results found.'); | |
} | |
}) | |
.catch(error => { | |
console.error('Error performing search:', error); | |
alert('Failed to perform search. Please try again.'); | |
}); | |
}); | |
backButton.addEventListener('click', () => { | |
window.location.href = '/'; | |
}); | |
}); | |
</script> | |
</body> | |
</html> |