feat: add frontend + backend + database to retrieve and compute news from Yahoo

This commit is contained in:
2026-04-18 23:53:57 +02:00
parent f9b6d35c49
commit 93668273ff
84 changed files with 15431 additions and 0 deletions

View File

@ -0,0 +1,146 @@
import { useState, useEffect } from 'react'
import { TrendingUp, Clock, Sparkles } from 'lucide-react'
import { summariesApi, type Summary } from '@/api/summaries'
import { assetsApi, type Asset } from '@/api/assets'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Spinner } from '@/components/ui/spinner'
import { useAuth } from '@/lib/auth'
function SummaryContent({ content }: { content: string }) {
const lines = content.split('\n')
return (
<div className="space-y-2 text-sm leading-relaxed">
{lines.map((line, i) => {
if (line.startsWith('## ')) return <h2 key={i} className="text-base font-semibold mt-4 first:mt-0">{line.slice(3)}</h2>
if (line.startsWith('### ')) return <h3 key={i} className="font-medium mt-3">{line.slice(4)}</h3>
if (line.startsWith('- ')) return <li key={i} className="ml-4 text-muted-foreground">{line.slice(2)}</li>
if (line.startsWith('**') && line.endsWith('**')) return <p key={i} className="font-semibold">{line.slice(2, -2)}</p>
if (line.trim() === '') return <div key={i} className="h-1" />
return <p key={i} className="text-muted-foreground">{line}</p>
})}
</div>
)
}
export function Dashboard() {
const { user } = useAuth()
const [summaries, setSummaries] = useState<Summary[]>([])
const [assets, setAssets] = useState<Asset[]>([])
const [loading, setLoading] = useState(true)
const [generating, setGenerating] = useState(false)
const [current, setCurrent] = useState<Summary | null>(null)
useEffect(() => { load() }, [])
async function load() {
setLoading(true)
try {
const [s, a] = await Promise.all([summariesApi.list(5), assetsApi.list()])
setSummaries(s ?? [])
setAssets(a ?? [])
setCurrent(s?.[0] ?? null)
} finally { setLoading(false) }
}
async function generate() {
setGenerating(true)
try {
const s = await summariesApi.generate()
setSummaries(prev => [s, ...prev])
setCurrent(s)
} catch (e) {
alert(e instanceof Error ? e.message : 'Erreur lors de la génération')
} finally { setGenerating(false) }
}
return (
<div className="p-4 md:p-6 space-y-6">
{/* Header */}
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-bold">Bonjour 👋</h1>
<p className="text-muted-foreground text-sm">{user?.email}</p>
</div>
<Button onClick={generate} disabled={generating || assets.length === 0}>
{generating ? <><Spinner className="h-4 w-4" /> Génération</> : <><Sparkles className="h-4 w-4" /> Générer un résumé</>}
</Button>
</div>
{assets.length === 0 && !loading && (
<Card className="border-dashed">
<CardContent className="py-8 text-center">
<TrendingUp className="h-10 w-10 mx-auto text-muted-foreground mb-3" />
<p className="font-medium">Votre watchlist est vide</p>
<p className="text-sm text-muted-foreground mt-1">Ajoutez des symboles dans la section Watchlist pour obtenir des résumés personnalisés</p>
</CardContent>
</Card>
)}
{assets.length > 0 && (
<div className="flex flex-wrap gap-2">
{assets.map(a => (
<Badge key={a.id} variant="secondary" className="font-mono">{a.symbol}</Badge>
))}
</div>
)}
{/* Résumé actuel */}
<div className="grid gap-6 lg:grid-cols-3">
<div className="lg:col-span-2 space-y-4">
{loading ? (
<div className="flex justify-center py-20"><Spinner /></div>
) : current ? (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Sparkles className="h-5 w-5 text-primary" /> Résumé IA
</CardTitle>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
{new Date(current.generated_at).toLocaleString('fr-FR')}
</div>
</div>
</CardHeader>
<CardContent>
<SummaryContent content={current.content} />
</CardContent>
</Card>
) : (
<Card className="border-dashed">
<CardContent className="py-12 text-center text-muted-foreground">
<Sparkles className="h-8 w-8 mx-auto mb-3 opacity-50" />
<p>Aucun résumé disponible</p>
<p className="text-xs mt-1">Cliquez sur "Générer un résumé" pour commencer</p>
</CardContent>
</Card>
)}
</div>
{/* Historique résumés */}
{summaries.length > 1 && (
<div className="space-y-3">
<h2 className="font-semibold text-sm text-muted-foreground uppercase tracking-wider">Historique</h2>
{summaries.slice(1).map(s => (
<Card
key={s.id}
className={`cursor-pointer transition-colors hover:border-primary/50 ${current?.id === s.id ? 'border-primary/50' : ''}`}
onClick={() => setCurrent(s)}
>
<CardContent className="py-3">
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-1">
<Clock className="h-3 w-3" />
{new Date(s.generated_at).toLocaleString('fr-FR')}
</div>
<p className="text-sm line-clamp-3">{s.content.slice(0, 120)}</p>
</CardContent>
</Card>
))}
</div>
)}
</div>
</div>
)
}