NeahNew/components/observatory/observatory-view.tsx
2025-05-04 17:14:11 +02:00

278 lines
9.1 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { RefreshCw, Globe } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ObservatoryMap } from "./observatory-map";
import { toast } from "@/components/ui/use-toast";
// News item interface matching the API response
interface NewsItem {
id: number;
title: string;
displayDate: string;
timestamp: string;
source: string;
description: string | null;
category: string | null;
url: string;
}
export function ObservatoryView() {
const [news, setNews] = useState<NewsItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedCountry, setSelectedCountry] = useState<string | null>(null);
const [isBrowser, setIsBrowser] = useState(false);
// Check if we're in the browser
useEffect(() => {
setIsBrowser(true);
}, []);
// Fetch news data
const fetchNews = async (forceRefresh = false) => {
setLoading(true);
try {
console.log('Requesting news with limit=100...');
const url = forceRefresh
? '/api/news?limit=100&refresh=true'
: '/api/news?limit=100';
console.log(`Fetching from: ${url}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch news');
}
const data = await response.json();
console.log(`Observatory received ${data.length} news articles`);
// Log first 5 articles for debugging
if (data.length > 0) {
console.log('First 5 articles:', data.slice(0, 5));
} else {
console.log('No articles received from API');
}
setNews(data);
setError(null);
} catch (err) {
setError('Failed to fetch news');
console.error('Error fetching news:', err);
} finally {
setLoading(false);
}
};
// Fetch news on component mount
useEffect(() => {
fetchNews();
}, []);
// Extract countries from news data (simplified version)
const extractCountries = (newsItems: NewsItem[]) => {
// This is a simplified implementation
// In a real app, we would use NLP or a more sophisticated technique
const countries = [
'France', 'USA', 'Canada', 'UK', 'Germany', 'Japan', 'China',
'India', 'Brazil', 'Australia', 'Russia', 'Italy', 'Spain',
'Sudan', 'New York', 'United Nations', 'Ukraine', 'Egypt',
'Mexico', 'South Africa', 'Nigeria', 'Argentina', 'Pakistan',
'Indonesia', 'Saudi Arabia', 'Iran', 'Turkey', 'South Korea',
'Thailand', 'Vietnam', 'Philippines', 'Malaysia', 'Singapore',
'Israel', 'Palestine', 'Syria', 'Iraq', 'Afghanistan',
'Morocco', 'Algeria', 'Tunisia', 'Kenya', 'Ethiopia',
'Greece', 'Poland', 'Sweden', 'Norway', 'Denmark', 'Finland',
'Netherlands', 'Belgium', 'Portugal', 'Switzerland', 'Austria'
];
// Sort countries by length (to prioritize longer names)
const sortedCountries = [...countries].sort((a, b) => b.length - a.length);
const result: Record<string, NewsItem[]> = {};
newsItems.forEach(item => {
// For title and description
const titleAndDesc = [
item.title || '',
item.description || ''
].join(' ').toLowerCase();
// Check each country
sortedCountries.forEach(country => {
if (titleAndDesc.includes(country.toLowerCase())) {
if (!result[country]) {
result[country] = [];
}
// Only add once per item
if (!result[country].some(existingItem => existingItem.id === item.id)) {
result[country].push(item);
}
}
});
});
return result;
};
// Handle country selection on the map
const handleCountrySelect = (country: string) => {
setSelectedCountry(selectedCountry === country ? null : country);
};
// Get news filtered by selected country
const getFilteredNews = () => {
if (!selectedCountry) return news;
const countriesMap = extractCountries(news);
const filtered = countriesMap[selectedCountry] || [];
console.log(`Filtered news for ${selectedCountry}: ${filtered.length} of ${news.length} total articles`);
return filtered;
};
// Invalidate cache
const invalidateCache = async () => {
try {
const response = await fetch('/api/news/purge-cache', {
method: 'POST'
});
if (!response.ok) {
throw new Error('Failed to invalidate cache');
}
toast({
title: "Cache purged",
description: "News cache has been invalidated. Fetching fresh data...",
});
// Fetch fresh data
fetchNews(true);
} catch (err) {
console.error('Error invalidating cache:', err);
toast({
title: "Error",
description: "Failed to invalidate cache",
variant: "destructive"
});
}
};
// Loading state
if (loading) {
return (
<div className="w-full h-[calc(100vh-2rem)] flex items-center justify-center">
<RefreshCw className="h-10 w-10 animate-spin text-gray-400" />
</div>
);
}
// Error state
if (error) {
return (
<div className="w-full h-[calc(100vh-2rem)] flex flex-col items-center justify-center">
<p className="text-red-500 mb-4">{error}</p>
<Button onClick={() => fetchNews(true)}>Retry</Button>
</div>
);
}
const filteredNews = getFilteredNews();
const countriesMap = extractCountries(news);
return (
<div className="w-full h-[calc(100vh-2rem)] bg-[#f5f4ef] pt-16 px-12">
{/* Main Content */}
<div className="grid grid-cols-2 gap-4 h-[calc(100vh-6rem)]">
{/* News Feed Section */}
<div>
<div className="bg-white rounded-lg overflow-hidden h-full flex flex-col">
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">
Latest News
<span className="text-sm font-normal ml-2 text-gray-500">
({filteredNews.length} articles)
</span>
</h2>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => invalidateCache()}
className="h-8 px-2 text-xs"
aria-label="Purge cache"
>
Purge Cache
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => fetchNews(true)}
className="h-8 w-8 p-0"
aria-label="Refresh news"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div className="overflow-y-auto flex-grow">
<div className="divide-y divide-gray-100">
{filteredNews.length === 0 ? (
<p className="text-gray-500 text-center py-10">No news available</p>
) : (
filteredNews.map(item => (
<div
key={item.id}
className="px-4 py-5 hover:bg-gray-50 transition-colors cursor-pointer"
onClick={() => window.open(item.url, '_blank')}
>
<div className="flex items-center justify-between text-xs text-gray-500 mb-1">
<span>{item.displayDate}</span>
<span>Unknown</span>
</div>
<h3 className="text-base font-medium text-gray-800 mb-1">
{item.title}
</h3>
<p className="text-sm text-gray-600 line-clamp-2">
{item.description}
</p>
</div>
))
)}
</div>
</div>
</div>
</div>
{/* Map Section */}
<div>
<div className="bg-white rounded-lg overflow-hidden h-full flex flex-col">
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">World Map</h2>
</div>
</div>
<div className="flex-grow">
{!loading && isBrowser && (
<ObservatoryMap
key="observatory-map"
countries={Object.entries(countriesMap).map(([name, items]) => ({
name,
count: items.length
}))}
onCountrySelect={handleCountrySelect}
selectedCountry={selectedCountry}
/>
)}
</div>
</div>
</div>
</div>
</div>
);
}