// 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(); loadCategories(); }); async function loadCategories() { try { const response = await fetch('/api/categories'); const data = await response.json(); const categories = data.categories || []; const container = document.getElementById('categoryCheckboxes'); container.innerHTML = ''; categories.forEach(category => { const label = document.createElement('label'); label.className = 'flex items-center space-x-3 cursor-pointer'; label.innerHTML = ` ${category.icon} ${category.name} `; container.appendChild(label); }); } catch (error) { console.error('Failed to load categories:', error); } } 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 let searchTimeout; async function handleSearch() { const searchInput = document.getElementById('searchInput'); const clearBtn = document.getElementById('clearSearch'); const searchStats = document.getElementById('searchStats'); const newsGrid = document.getElementById('newsGrid'); searchQuery = searchInput.value.trim(); // Show/hide clear button if (searchQuery) { clearBtn.classList.remove('hidden'); } else { clearBtn.classList.add('hidden'); } // Clear previous timeout if (searchTimeout) clearTimeout(searchTimeout); // If empty query, reset to all articles if (searchQuery === '') { filteredArticles = allArticles; displayedCount = 0; newsGrid.innerHTML = ''; updateSearchStats(); loadMoreArticles(); return; } // Debounce search API call searchTimeout = setTimeout(async () => { // Show searching state newsGrid.innerHTML = '
Searching...
'; try { const response = await fetch(`/api/search?q=${encodeURIComponent(searchQuery)}&limit=20`); // Check if response is ok if (!response.ok) { const errorText = await response.text(); throw new Error(`Server returned ${response.status}: ${errorText}`); } const data = await response.json(); if (data.results && data.results.length > 0) { // Map results to match card format filteredArticles = data.results.map(item => ({ title: item.title, link: item.link, source: item.source, summary: item.snippet, // Map snippet to summary published_at: item.published_at, score: item.relevance_score })); displayedCount = 0; newsGrid.innerHTML = ''; // Update stats searchStats.textContent = `Found ${filteredArticles.length} relevant articles`; loadMoreArticles(); } else { newsGrid.innerHTML = `
🔍

No relevant articles found

Try different keywords or concepts

`; searchStats.textContent = 'No results found'; } } catch (error) { console.error('Search failed:', error); newsGrid.innerHTML = `
Search failed: ${error.message}
`; } }, 500); // 500ms debounce } 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; } // Get selected categories const checkboxes = document.querySelectorAll('#categoryCheckboxes input[type="checkbox"]:checked'); const categories = Array.from(checkboxes).map(cb => cb.value); if (categories.length === 0) { formMessage.textContent = 'Please select at least one category'; 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, categories: categories }) }); 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(); } }