216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
import { fr } from 'date-fns/locale';
|
|
import Link from 'next/link';
|
|
import { Button } from '@/components/ui/button';
|
|
import { RefreshCw } from 'lucide-react';
|
|
|
|
interface NewsItem {
|
|
id: number;
|
|
title: string;
|
|
url: string;
|
|
date: string;
|
|
source: string;
|
|
description: string;
|
|
category: string;
|
|
sentiment: {
|
|
score: number | null;
|
|
label: string | null;
|
|
};
|
|
symbols: string[] | null;
|
|
symbol: string | null;
|
|
}
|
|
|
|
export function News() {
|
|
console.log('[News] Component mounting...');
|
|
|
|
const [news, setNews] = useState<NewsItem[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [dbStatus, setDbStatus] = useState<'connecting' | 'connected' | 'error'>('connecting');
|
|
|
|
// Debug info display component
|
|
const DebugInfo = () => {
|
|
console.log('[News] Current state:', {
|
|
newsCount: news.length,
|
|
loading,
|
|
error,
|
|
dbStatus,
|
|
newsItems: news
|
|
});
|
|
|
|
return (
|
|
<div className="text-xs text-gray-500 mt-2 p-2 bg-gray-100 rounded">
|
|
<p>Status: {dbStatus}</p>
|
|
<p>Loading: {loading ? 'true' : 'false'}</p>
|
|
<p>Error: {error || 'none'}</p>
|
|
<p>News items: {news.length}</p>
|
|
<details>
|
|
<summary>Debug Details</summary>
|
|
<pre className="mt-2 text-[10px] whitespace-pre-wrap">
|
|
{JSON.stringify(news, null, 2)}
|
|
</pre>
|
|
</details>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const fetchNews = async (isRefresh = false) => {
|
|
console.log('[News] Fetching news, isRefresh:', isRefresh);
|
|
|
|
if (isRefresh) setRefreshing(true);
|
|
setLoading(true);
|
|
setDbStatus('connecting');
|
|
|
|
try {
|
|
console.log('[News] Making API request to /api/news');
|
|
const response = await fetch('/api/news', {
|
|
method: 'GET',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
},
|
|
});
|
|
|
|
console.log('[News] API response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('[News] API response data:', data);
|
|
|
|
if (data.error) {
|
|
throw new Error(data.error);
|
|
}
|
|
|
|
console.log('[News] Setting news items:', data.news);
|
|
setNews(data.news || []);
|
|
setError(null);
|
|
setDbStatus('connected');
|
|
} catch (err) {
|
|
console.error('[News] Error fetching news:', err);
|
|
const errorMessage = err instanceof Error ? err.message : 'Failed to load news';
|
|
setError(errorMessage);
|
|
setDbStatus('error');
|
|
setNews([]);
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
console.log('[News] Running useEffect...');
|
|
fetchNews();
|
|
return () => {
|
|
console.log('[News] Component unmounting...');
|
|
};
|
|
}, []);
|
|
|
|
if (loading && !refreshing) {
|
|
return (
|
|
<Card className="w-full">
|
|
<CardHeader>
|
|
<CardTitle>News</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-col items-center justify-center h-32 space-y-2">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
|
<p className="text-sm text-gray-500">
|
|
{dbStatus === 'connecting' ? 'Connecting to database...' : 'Loading news...'}
|
|
</p>
|
|
</div>
|
|
<DebugInfo />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className="w-full">
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<CardTitle>News</CardTitle>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => fetchNews(true)}
|
|
disabled={refreshing}
|
|
className={`${refreshing ? 'animate-spin' : ''}`}
|
|
>
|
|
<RefreshCw className="h-4 w-4" />
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{error ? (
|
|
<div className="text-red-500 space-y-2">
|
|
<p>{error}</p>
|
|
<p className="text-sm text-gray-500">
|
|
{dbStatus === 'error' ? 'Database connection error' : 'Failed to fetch news'}
|
|
</p>
|
|
</div>
|
|
) : news.length === 0 ? (
|
|
<div className="text-center text-gray-500 py-8">
|
|
No news available
|
|
</div>
|
|
) : (
|
|
news.map((item) => (
|
|
<div key={item.id} className="border-b pb-4 last:border-b-0">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1">
|
|
<Link
|
|
href={item.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-blue-600 hover:underline font-medium"
|
|
>
|
|
{item.title}
|
|
</Link>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
{item.source} • {formatDistanceToNow(new Date(item.date), { addSuffix: true, locale: fr })}
|
|
</p>
|
|
{item.description && (
|
|
<p className="text-sm text-gray-600 mt-2 line-clamp-2">
|
|
{item.description.replace(/<[^>]*>/g, '')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
{item.sentiment.score !== null && (
|
|
<div className={`ml-4 px-2 py-1 rounded text-sm ${
|
|
item.sentiment.score > 0 ? 'bg-green-100 text-green-800' :
|
|
item.sentiment.score < 0 ? 'bg-red-100 text-red-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{item.sentiment.label || 'Neutral'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{(item.symbols?.length || item.symbol) && (
|
|
<div className="mt-2 flex flex-wrap gap-2">
|
|
{item.symbols?.map((symbol, index) => (
|
|
<span key={index} className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
|
|
{symbol}
|
|
</span>
|
|
))}
|
|
{item.symbol && !item.symbols?.includes(item.symbol) && (
|
|
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded">
|
|
{item.symbol}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
<DebugInfo />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|