NeahFront7/components/news.tsx
2025-04-13 22:59:09 +02:00

243 lines
7.6 KiB
TypeScript

"use client";
import { useEffect, useState, useCallback } 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;
}
interface DebugState {
newsCount: number;
loading: boolean;
error: string | null;
dbStatus: string;
lastUpdate: string;
lastError: string | null;
}
export function News() {
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');
const [debugState, setDebugState] = useState<DebugState>({
newsCount: 0,
loading: true,
error: null,
dbStatus: 'connecting',
lastUpdate: new Date().toISOString(),
lastError: null
});
const updateDebugState = useCallback((updates: Partial<DebugState>) => {
setDebugState(prev => {
const newState = { ...prev, ...updates, lastUpdate: new Date().toISOString() };
console.table(newState);
return newState;
});
}, []);
const fetchNews = useCallback(async (isRefresh = false) => {
updateDebugState({ loading: true, dbStatus: 'connecting' });
if (isRefresh) {
setRefreshing(true);
updateDebugState({ lastError: null });
}
setLoading(true);
setDbStatus('connecting');
try {
updateDebugState({ dbStatus: 'fetching' });
const response = await fetch('/api/news');
updateDebugState({ dbStatus: response.ok ? 'received' : 'error' });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
setNews(data.news || []);
setError(null);
setDbStatus('connected');
updateDebugState({
newsCount: (data.news || []).length,
error: null,
dbStatus: 'connected',
loading: false
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load news';
setError(errorMessage);
setDbStatus('error');
setNews([]);
updateDebugState({
error: errorMessage,
dbStatus: 'error',
loading: false,
lastError: errorMessage
});
} finally {
setLoading(false);
setRefreshing(false);
}
}, [updateDebugState]);
useEffect(() => {
updateDebugState({ dbStatus: 'initializing' });
fetchNews();
return () => {
updateDebugState({ dbStatus: 'unmounting' });
};
}, [fetchNews, updateDebugState]);
const DebugInfo = () => (
<div className="text-xs text-gray-500 mt-2 p-2 bg-gray-100 rounded">
<p>Status: {debugState.dbStatus}</p>
<p>Loading: {debugState.loading ? 'true' : 'false'}</p>
<p>Error: {debugState.error || 'none'}</p>
<p>News items: {debugState.newsCount}</p>
<p>Last update: {new Date(debugState.lastUpdate).toLocaleTimeString()}</p>
{debugState.lastError && (
<p className="text-red-500">Last error: {debugState.lastError}</p>
)}
<details>
<summary>Debug Details</summary>
<pre className="mt-2 text-[10px] whitespace-pre-wrap">
{JSON.stringify(news, null, 2)}
</pre>
</details>
</div>
);
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>
);
}