feat: add frontend + backend + database to retrieve and compute news from Yahoo
This commit is contained in:
146
frontend/src/pages/Dashboard.tsx
Normal file
146
frontend/src/pages/Dashboard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user