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

212 lines
7.0 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";
// 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 () => {
setLoading(true);
try {
const response = await fetch('/api/news?limit=100');
if (!response.ok) {
throw new Error('Failed to fetch news');
}
const data = await response.json();
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);
return countriesMap[selectedCountry] || [];
};
// 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}>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>
</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>
);
}