// 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 = '
Loading news...
'; try { const response = await fetch('/api/news'); const data = await response.json(); if (data.articles && data.articles.length > 0) { allArticles = data.articles; filteredArticles = data.articles; displayedCount = 0; newsGrid.innerHTML = ''; updateSearchStats(); loadMoreArticles(); } else { newsGrid.innerHTML = '
No news available at the moment. Check back later!
'; } } catch (error) { console.error('Error loading news:', error); newsGrid.innerHTML = '
Failed to load news. Please try again later.
'; } } 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 = '

Loading more...

'; 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 = ''; // Update stats updateSearchStats(); // Load filtered articles if (filteredArticles.length > 0) { loadMoreArticles(); } else { newsGrid.innerHTML = `
🔍

No articles found

Try a different search term

`; } } 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(']*>/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 = `
${imageUrl ? `${article.title}` : sourceIcon}
${sourceName} ${readTime ? `📖 ${readTime} min read` : ''}

${article.title}

${cleanSummary}

`; // 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() { try { const response = await fetch('/api/stats'); const data = await response.json(); if (data.subscribers !== undefined) { document.getElementById('subscriberCount').textContent = data.subscribers.toLocaleString(); } } catch (error) { console.error('Error loading stats:', error); } } async function subscribe() { const emailInput = document.getElementById('emailInput'); const subscribeBtn = document.getElementById('subscribeBtn'); const formMessage = document.getElementById('formMessage'); const email = emailInput.value.trim(); if (!email || !email.includes('@')) { formMessage.textContent = 'Please enter a valid email address'; 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 { const response = await fetch('/api/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }); const data = await response.json(); if (response.ok) { formMessage.textContent = data.message || 'Successfully subscribed! Check your email for confirmation.'; 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 = 'text-red-200 font-medium'; } } catch (error) { formMessage.textContent = 'Network error. Please try again later.'; formMessage.className = 'text-red-200 font-medium'; } finally { subscribeBtn.disabled = false; subscribeBtn.textContent = 'Subscribe Free'; subscribeBtn.classList.remove('opacity-75', 'cursor-not-allowed'); } } // Allow Enter key to submit document.getElementById('emailInput').addEventListener('keypress', (e) => { if (e.key === 'Enter') { subscribe(); } }); function showUnsubscribe() { document.getElementById('unsubscribeModal').classList.remove('hidden'); } function closeUnsubscribe() { document.getElementById('unsubscribeModal').classList.add('hidden'); document.getElementById('unsubscribeEmail').value = ''; document.getElementById('unsubscribeMessage').textContent = ''; document.getElementById('unsubscribeMessage').className = ''; } async function unsubscribe() { const emailInput = document.getElementById('unsubscribeEmail'); const unsubscribeMessage = document.getElementById('unsubscribeMessage'); const email = emailInput.value.trim(); if (!email || !email.includes('@')) { unsubscribeMessage.textContent = 'Please enter a valid email address'; unsubscribeMessage.className = 'text-red-600 font-medium'; return; } try { const response = await fetch('/api/unsubscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email }) }); const data = await response.json(); if (response.ok) { unsubscribeMessage.textContent = data.message || 'Successfully unsubscribed.'; unsubscribeMessage.className = 'text-green-600 font-medium'; emailInput.value = ''; setTimeout(() => { closeUnsubscribe(); loadStats(); }, 2000); } else { unsubscribeMessage.textContent = data.error || 'Failed to unsubscribe. Please try again.'; unsubscribeMessage.className = 'text-red-600 font-medium'; } } catch (error) { unsubscribeMessage.textContent = 'Network error. Please try again later.'; unsubscribeMessage.className = 'text-red-600 font-medium'; } } // Close modal when clicking outside window.onclick = function(event) { const modal = document.getElementById('unsubscribeModal'); if (event.target === modal) { closeUnsubscribe(); } }