Files
Munich-news/frontend/public/app.js
2025-11-12 22:33:56 +01:00

433 lines
15 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();
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 = `
<input type="checkbox" value="${category.id}" checked class="w-5 h-5 rounded border-2 border-white/30 bg-white/20 checked:bg-white checked:border-white focus:ring-2 focus:ring-white/50">
<span class="text-white text-sm">${category.icon} ${category.name}</span>
`;
container.appendChild(label);
});
} catch (error) {
console.error('Failed to load categories:', error);
}
}
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;
}
// 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();
}
}