This commit is contained in:
2025-12-10 15:50:11 +00:00
parent 50b9888004
commit 4e8b60f77c
12 changed files with 247 additions and 106 deletions

View File

@@ -19,10 +19,10 @@ async function loadCategories() {
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';
@@ -40,11 +40,11 @@ async function loadCategories() {
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;
@@ -63,24 +63,24 @@ async function loadNews() {
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');
@@ -95,17 +95,17 @@ function loadMoreArticles() {
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();
}
@@ -113,53 +113,85 @@ function setupInfiniteScroll() {
}
// Search functionality
function handleSearch() {
let searchTimeout;
async function handleSearch() {
const searchInput = document.getElementById('searchInput');
const clearBtn = document.getElementById('clearSearch');
searchQuery = searchInput.value.trim().toLowerCase();
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');
}
// Filter articles
// Clear previous timeout
if (searchTimeout) clearTimeout(searchTimeout);
// If empty query, reset to all 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) {
displayedCount = 0;
newsGrid.innerHTML = '';
updateSearchStats();
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>
`;
return;
}
// Debounce search API call
searchTimeout = setTimeout(async () => {
// Show searching state
newsGrid.innerHTML = '<div class="text-center py-10 text-gray-500">Searching...</div>';
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 = `
<div class="text-center py-16">
<div class="text-6xl mb-4">🔍</div>
<p class="text-xl text-gray-600 mb-2">No relevant articles found</p>
<p class="text-gray-400">Try different keywords or concepts</p>
</div>
`;
searchStats.textContent = 'No results found';
}
} catch (error) {
console.error('Search failed:', error);
newsGrid.innerHTML = `<div class="text-center py-10 text-red-400">Search failed: ${error.message}</div>`;
}
}, 500); // 500ms debounce
}
function clearSearch() {
@@ -182,11 +214,11 @@ 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) {
@@ -195,17 +227,17 @@ function createNewsCard(article, index) {
// 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 -->
@@ -237,11 +269,11 @@ function createNewsCard(article, index) {
</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;
}
@@ -293,7 +325,7 @@ 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();
}
@@ -306,44 +338,44 @@ 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({
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';
@@ -384,15 +416,15 @@ function closeUnsubscribe() {
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',
@@ -401,9 +433,9 @@ async function unsubscribe() {
},
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';
@@ -423,7 +455,7 @@ async function unsubscribe() {
}
// Close modal when clicking outside
window.onclick = function(event) {
window.onclick = function (event) {
const modal = document.getElementById('unsubscribeModal');
if (event.target === modal) {
closeUnsubscribe();