SPB Git

spb/vquant Public MIT

VibeQuant — AI-powered institutional-grade financial intelligence platform.

TypeScript 84.3% Python 11.7% JavaScript 1.6% CSS 1.5% HTML 0.7%
5.2 KB · 158 lines tsx
Raw Blame History
1/*2 * =============================================================================3 *  VibeQuant (vquant) — AI-Powered Financial Intelligence Platform4 * -----------------------------------------------------------------------------5 *  File:      client/src/components/explore/news-view.tsx6 *7 *  Author:    Simon-Pierre Boucher8 *  Contact:   contact@spboucher.ai9 *  Website:   https://www.spboucher.ai10 *  Demo:      https://www.vquant.ai11 *  License:   MIT (see LICENSE)12 *13 *  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.14 * =============================================================================15 */1617import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";18import { Badge } from "@/components/ui/badge";19import { Newspaper, ExternalLink, Calendar, TrendingUp } from "lucide-react";2021interface NewsArticle {22  title: string;23  text: string;24  url: string;25  site: string;26  publishedDate: string;27  symbol: string;28  image?: string;29  [key: string]: any;30}3132interface NewsViewProps {33  news?: NewsArticle[];34}3536export function NewsView({ news }: NewsViewProps) {37  const formatDate = (dateString: string) => {38    const date = new Date(dateString);39    const now = new Date();40    const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));4142    if (diffInHours < 1) return "Il y a moins d'une heure";43    if (diffInHours < 24) return `Il y a ${diffInHours} heure${diffInHours > 1 ? 's' : ''}`;44    if (diffInHours < 48) return "Hier";4546    return date.toLocaleDateString('fr-FR', {47      year: 'numeric',48      month: 'long',49      day: 'numeric',50      hour: '2-digit',51      minute: '2-digit'52    });53  };5455  if (!news || news.length === 0) {56    return (57      <Card>58        <CardHeader>59          <CardTitle className="flex items-center gap-2">60            <Newspaper className="w-5 h-5" />61            Actualités Financières62          </CardTitle>63        </CardHeader>64        <CardContent>65          <p className="text-muted-foreground">Aucune actualité disponible</p>66        </CardContent>67      </Card>68    );69  }7071  return (72    <div className="space-y-6">73      <div className="flex items-center gap-3 mb-4">74        <div className="p-2 rounded-lg bg-primary/10">75          <Newspaper className="w-6 h-6 text-primary" />76        </div>77        <div>78          <h2 className="text-2xl font-bold">Actualités & Analyses</h2>79          <p className="text-muted-foreground">80            {news.length} article{news.length > 1 ? 's' : ''} récent{news.length > 1 ? 's' : ''}81          </p>82        </div>83      </div>8485      <div className="grid grid-cols-1 gap-4">86        {news.map((article, idx) => (87          <Card88            key={idx}89            className="hover:shadow-lg transition-all duration-300 hover:border-primary/50 cursor-pointer"90            onClick={() => window.open(article.url, '_blank')}91          >92            <CardContent className="pt-6">93              <div className="flex gap-4">94                {/* Image */}95                {article.image && (96                  <div className="flex-shrink-0 w-48 h-32 rounded-lg overflow-hidden bg-muted">97                    <img98                      src={article.image}99                      alt={article.title}100                      className="w-full h-full object-cover"101                      onError={(e) => {102                        (e.target as HTMLImageElement).style.display = 'none';103                      }}104                    />105                  </div>106                )}107108                {/* Content */}109                <div className="flex-1 min-w-0">110                  <div className="flex items-start justify-between gap-4 mb-2">111                    <h3 className="text-xl font-bold line-clamp-2 hover:text-primary transition-colors">112                      {article.title}113                    </h3>114                    <ExternalLink className="w-5 h-5 flex-shrink-0 text-muted-foreground" />115                  </div>116117                  <p className="text-muted-foreground line-clamp-3 mb-3">118                    {article.text}119                  </p>120121                  <div className="flex items-center gap-3 flex-wrap">122                    <Badge variant="outline" className="gap-1">123                      <Calendar className="w-3 h-3" />124                      {formatDate(article.publishedDate)}125                    </Badge>126127                    <Badge variant="secondary">128                      {article.site}129                    </Badge>130131                    {article.symbol && (132                      <Badge variant="default">133                        {article.symbol}134                      </Badge>135                    )}136                  </div>137                </div>138              </div>139            </CardContent>140          </Card>141        ))}142      </div>143144      {/* Load More Button */}145      {news.length >= 10 && (146        <Card className="border-dashed">147          <CardContent className="flex items-center justify-center py-8">148            <button className="text-primary hover:underline font-semibold flex items-center gap-2">149              <TrendingUp className="w-4 h-4" />150              Charger plus d'actualités151            </button>152          </CardContent>153        </Card>154      )}155    </div>156  );157}158