396 lines
14 KiB
JavaScript
396 lines
14 KiB
JavaScript
// 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="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) {
|
|
allArticles = data.articles;
|
|
filteredArticles = data.articles;
|
|
displayedCount = 0;
|
|
newsGrid.innerHTML = '';
|
|
updateSearchStats();
|
|
loadMoreArticles();
|
|
} else {
|
|
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="text-center py-10 text-gray-500">Failed to load news. Please try again later.</div>';
|
|
}
|
|
}
|
|
|
|
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 = '';
|
|
|
|
// 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>
|
|
`;
|
|
}
|
|
}
|
|
|
|
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() {
|
|
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();
|
|
}
|
|
}
|
|
|