update
This commit is contained in:
@@ -1,46 +1,268 @@
|
||||
// Pagination state
|
||||
let allArticles = [];
|
||||
let filteredArticles = [];
|
||||
let displayedCount = 0;
|
||||
const ARTICLES_PER_PAGE = 5;
|
||||
let isLoading = false;
|
||||
let searchQuery = '';
|
||||
|
||||
// Load news on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadNews();
|
||||
loadStats();
|
||||
setupInfiniteScroll();
|
||||
});
|
||||
|
||||
async function loadNews() {
|
||||
const newsGrid = document.getElementById('newsGrid');
|
||||
newsGrid.innerHTML = '<div class="loading">Loading news...</div>';
|
||||
newsGrid.innerHTML = '<div class="text-center py-10 text-gray-500">Loading news...</div>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/news');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.articles && data.articles.length > 0) {
|
||||
displayNews(data.articles);
|
||||
allArticles = data.articles;
|
||||
filteredArticles = data.articles;
|
||||
displayedCount = 0;
|
||||
newsGrid.innerHTML = '';
|
||||
updateSearchStats();
|
||||
loadMoreArticles();
|
||||
} else {
|
||||
newsGrid.innerHTML = '<div class="loading">No news available at the moment. Check back later!</div>';
|
||||
newsGrid.innerHTML = '<div class="text-center py-10 text-gray-500">No news available at the moment. Check back later!</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading news:', error);
|
||||
newsGrid.innerHTML = '<div class="loading">Failed to load news. Please try again later.</div>';
|
||||
newsGrid.innerHTML = '<div class="text-center py-10 text-gray-500">Failed to load news. Please try again later.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function displayNews(articles) {
|
||||
function loadMoreArticles() {
|
||||
if (isLoading || displayedCount >= filteredArticles.length) return;
|
||||
|
||||
isLoading = true;
|
||||
const newsGrid = document.getElementById('newsGrid');
|
||||
|
||||
// Remove loading indicator if exists
|
||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||
if (loadingIndicator) loadingIndicator.remove();
|
||||
|
||||
// Get next batch of articles
|
||||
const nextBatch = filteredArticles.slice(displayedCount, displayedCount + ARTICLES_PER_PAGE);
|
||||
|
||||
nextBatch.forEach((article, index) => {
|
||||
const card = createNewsCard(article, displayedCount + index);
|
||||
newsGrid.appendChild(card);
|
||||
});
|
||||
|
||||
displayedCount += nextBatch.length;
|
||||
|
||||
// Add loading indicator if more articles available
|
||||
if (displayedCount < filteredArticles.length) {
|
||||
const loader = document.createElement('div');
|
||||
loader.id = 'loadingIndicator';
|
||||
loader.className = 'text-center py-8 text-gray-400';
|
||||
loader.innerHTML = '<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div><p class="mt-2">Loading more...</p>';
|
||||
newsGrid.appendChild(loader);
|
||||
} else if (filteredArticles.length > 0) {
|
||||
// Add end message
|
||||
const endMessage = document.createElement('div');
|
||||
endMessage.className = 'text-center py-8 text-gray-400 text-sm';
|
||||
endMessage.textContent = `✓ All ${filteredArticles.length} articles loaded`;
|
||||
newsGrid.appendChild(endMessage);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function setupInfiniteScroll() {
|
||||
window.addEventListener('scroll', () => {
|
||||
if (isLoading || displayedCount >= filteredArticles.length) return;
|
||||
|
||||
const scrollPosition = window.innerHeight + window.scrollY;
|
||||
const threshold = document.documentElement.scrollHeight - 500;
|
||||
|
||||
if (scrollPosition >= threshold) {
|
||||
loadMoreArticles();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
function handleSearch() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const clearBtn = document.getElementById('clearSearch');
|
||||
searchQuery = searchInput.value.trim().toLowerCase();
|
||||
|
||||
// Show/hide clear button
|
||||
if (searchQuery) {
|
||||
clearBtn.classList.remove('hidden');
|
||||
} else {
|
||||
clearBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Filter articles
|
||||
if (searchQuery === '') {
|
||||
filteredArticles = allArticles;
|
||||
} else {
|
||||
filteredArticles = allArticles.filter(article => {
|
||||
const title = article.title.toLowerCase();
|
||||
const summary = (article.summary || '').toLowerCase().replace(/<[^>]*>/g, '');
|
||||
const source = formatSourceName(article.source).toLowerCase();
|
||||
|
||||
return title.includes(searchQuery) ||
|
||||
summary.includes(searchQuery) ||
|
||||
source.includes(searchQuery);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset display
|
||||
displayedCount = 0;
|
||||
const newsGrid = document.getElementById('newsGrid');
|
||||
newsGrid.innerHTML = '';
|
||||
|
||||
articles.forEach(article => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'news-card';
|
||||
card.onclick = () => window.open(article.link, '_blank');
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="source">${article.source || 'Munich News'}</div>
|
||||
<h3>${article.title}</h3>
|
||||
<p>${article.summary || 'No summary available.'}</p>
|
||||
<a href="${article.link}" target="_blank" class="read-more" onclick="event.stopPropagation()">Read more →</a>
|
||||
// Update stats
|
||||
updateSearchStats();
|
||||
|
||||
// Load filtered articles
|
||||
if (filteredArticles.length > 0) {
|
||||
loadMoreArticles();
|
||||
} else {
|
||||
newsGrid.innerHTML = `
|
||||
<div class="text-center py-16">
|
||||
<div class="text-6xl mb-4">🔍</div>
|
||||
<p class="text-xl text-gray-600 mb-2">No articles found</p>
|
||||
<p class="text-gray-400">Try a different search term</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
newsGrid.appendChild(card);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
searchInput.value = '';
|
||||
handleSearch();
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
function updateSearchStats() {
|
||||
const searchStats = document.getElementById('searchStats');
|
||||
if (searchQuery) {
|
||||
searchStats.textContent = `Found ${filteredArticles.length} of ${allArticles.length} articles`;
|
||||
} else {
|
||||
searchStats.textContent = `Showing ${allArticles.length} articles`;
|
||||
}
|
||||
}
|
||||
|
||||
function createNewsCard(article, index) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'group bg-white rounded-xl overflow-hidden shadow-md hover:shadow-xl transition-all duration-300 cursor-pointer border border-gray-100 hover:border-primary/30';
|
||||
card.onclick = () => window.open(article.link, '_blank');
|
||||
|
||||
// Extract image from summary if it's an img tag (from Süddeutsche)
|
||||
let imageUrl = null;
|
||||
let cleanSummary = article.summary || 'No summary available.';
|
||||
|
||||
if (cleanSummary.includes('<img')) {
|
||||
const imgMatch = cleanSummary.match(/src="([^"]+)"/);
|
||||
if (imgMatch) {
|
||||
imageUrl = imgMatch[1];
|
||||
}
|
||||
// Remove img tag from summary
|
||||
cleanSummary = cleanSummary.replace(/<img[^>]*>/g, '').replace(/<\/?p>/g, '').trim();
|
||||
}
|
||||
|
||||
// Get source icon/emoji
|
||||
const sourceIcon = getSourceIcon(article.source);
|
||||
|
||||
// Format source name
|
||||
const sourceName = formatSourceName(article.source);
|
||||
|
||||
// Get word count badge
|
||||
const wordCount = article.word_count || article.summary_word_count;
|
||||
const readTime = wordCount ? Math.ceil(wordCount / 200) : null;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="flex flex-col sm:flex-row">
|
||||
<!-- Image -->
|
||||
<div class="relative w-full sm:w-64 h-48 sm:h-auto bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-white text-5xl overflow-hidden flex-shrink-0">
|
||||
${imageUrl ? `<img src="${imageUrl}" alt="${article.title}" class="w-full h-full object-cover" onerror="this.style.display='none'; this.parentElement.innerHTML='${sourceIcon}';">` : sourceIcon}
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="p-6 flex flex-col flex-1">
|
||||
<!-- Source Badge -->
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="w-1.5 h-1.5 bg-primary rounded-full"></span>
|
||||
<span class="text-xs font-bold text-primary uppercase tracking-wide">${sourceName}</span>
|
||||
${readTime ? `<span class="text-xs text-gray-400 ml-auto">📖 ${readTime} min read</span>` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-3 leading-tight group-hover:text-primary transition-colors">${article.title}</h3>
|
||||
|
||||
<!-- Summary -->
|
||||
<p class="text-sm text-gray-600 mb-4 leading-relaxed line-clamp-2">${cleanSummary}</p>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center gap-4 mt-auto">
|
||||
<a href="${article.link}" target="_blank" onclick="event.stopPropagation()" class="text-primary font-semibold text-sm hover:gap-2 flex items-center gap-1 transition-all">
|
||||
Read full article <span class="group-hover:translate-x-1 transition-transform">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add staggered animation
|
||||
card.style.opacity = '0';
|
||||
card.style.animation = `fadeIn 0.5s ease-out ${(index % ARTICLES_PER_PAGE) * 0.1}s forwards`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// Add animation keyframes
|
||||
if (!document.getElementById('animations')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'animations';
|
||||
style.textContent = `
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function getSourceIcon(source) {
|
||||
const icons = {
|
||||
'abendzeitung-muenchen': '📰',
|
||||
'sueddeutsche': '📄',
|
||||
'muenchen': '🏛️',
|
||||
'default': '📰'
|
||||
};
|
||||
return icons[source] || icons.default;
|
||||
}
|
||||
|
||||
function formatSourceName(source) {
|
||||
const names = {
|
||||
'abendzeitung-muenchen': 'Abendzeitung München',
|
||||
'sueddeutsche': 'Süddeutsche Zeitung',
|
||||
'muenchen': 'München.de'
|
||||
};
|
||||
return names[source] || source.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
@@ -65,12 +287,13 @@ async function subscribe() {
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
formMessage.textContent = 'Please enter a valid email address';
|
||||
formMessage.className = 'form-message error';
|
||||
formMessage.className = 'text-red-200 font-medium';
|
||||
return;
|
||||
}
|
||||
|
||||
subscribeBtn.disabled = true;
|
||||
subscribeBtn.textContent = 'Subscribing...';
|
||||
subscribeBtn.classList.add('opacity-75', 'cursor-not-allowed');
|
||||
formMessage.textContent = '';
|
||||
|
||||
try {
|
||||
@@ -86,19 +309,20 @@ async function subscribe() {
|
||||
|
||||
if (response.ok) {
|
||||
formMessage.textContent = data.message || 'Successfully subscribed! Check your email for confirmation.';
|
||||
formMessage.className = 'form-message success';
|
||||
formMessage.className = 'text-green-200 font-medium';
|
||||
emailInput.value = '';
|
||||
loadStats(); // Refresh stats
|
||||
} else {
|
||||
formMessage.textContent = data.error || 'Failed to subscribe. Please try again.';
|
||||
formMessage.className = 'form-message error';
|
||||
formMessage.className = 'text-red-200 font-medium';
|
||||
}
|
||||
} catch (error) {
|
||||
formMessage.textContent = 'Network error. Please try again later.';
|
||||
formMessage.className = 'form-message error';
|
||||
formMessage.className = 'text-red-200 font-medium';
|
||||
} finally {
|
||||
subscribeBtn.disabled = false;
|
||||
subscribeBtn.textContent = 'Subscribe Free';
|
||||
subscribeBtn.classList.remove('opacity-75', 'cursor-not-allowed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,13 +334,14 @@ document.getElementById('emailInput').addEventListener('keypress', (e) => {
|
||||
});
|
||||
|
||||
function showUnsubscribe() {
|
||||
document.getElementById('unsubscribeModal').style.display = 'block';
|
||||
document.getElementById('unsubscribeModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeUnsubscribe() {
|
||||
document.getElementById('unsubscribeModal').style.display = 'none';
|
||||
document.getElementById('unsubscribeModal').classList.add('hidden');
|
||||
document.getElementById('unsubscribeEmail').value = '';
|
||||
document.getElementById('unsubscribeMessage').textContent = '';
|
||||
document.getElementById('unsubscribeMessage').className = '';
|
||||
}
|
||||
|
||||
async function unsubscribe() {
|
||||
@@ -127,7 +352,7 @@ async function unsubscribe() {
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
unsubscribeMessage.textContent = 'Please enter a valid email address';
|
||||
unsubscribeMessage.className = 'form-message error';
|
||||
unsubscribeMessage.className = 'text-red-600 font-medium';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -144,7 +369,7 @@ async function unsubscribe() {
|
||||
|
||||
if (response.ok) {
|
||||
unsubscribeMessage.textContent = data.message || 'Successfully unsubscribed.';
|
||||
unsubscribeMessage.className = 'form-message success';
|
||||
unsubscribeMessage.className = 'text-green-600 font-medium';
|
||||
emailInput.value = '';
|
||||
setTimeout(() => {
|
||||
closeUnsubscribe();
|
||||
@@ -152,11 +377,11 @@ async function unsubscribe() {
|
||||
}, 2000);
|
||||
} else {
|
||||
unsubscribeMessage.textContent = data.error || 'Failed to unsubscribe. Please try again.';
|
||||
unsubscribeMessage.className = 'form-message error';
|
||||
unsubscribeMessage.className = 'text-red-600 font-medium';
|
||||
}
|
||||
} catch (error) {
|
||||
unsubscribeMessage.textContent = 'Network error. Please try again later.';
|
||||
unsubscribeMessage.className = 'form-message error';
|
||||
unsubscribeMessage.className = 'text-red-600 font-medium';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user