浏览代码

Feat: Show article content in modal

Implement a modal to display the full article content when clicking on the news card. If the article is a YouTube video, embed and play the video within the modal.
gpt-engineer-app[bot] 6 月之前
父节点
当前提交
ab5c64d93e
共有 3 个文件被更改,包括 190 次插入9 次删除
  1. 135 0
      src/components/ArticleModal.tsx
  2. 33 8
      src/components/NewsCard.tsx
  3. 22 1
      src/pages/Index.tsx

+ 135 - 0
src/components/ArticleModal.tsx

@@ -0,0 +1,135 @@
+
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from '@/components/ui/dialog';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { NewsItem } from '@/types/news';
+import { 
+  Clock, 
+  ExternalLink,
+  Calendar
+} from 'lucide-react';
+
+interface ArticleModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  article: NewsItem | null;
+}
+
+const ArticleModal = ({ isOpen, onClose, article }: ArticleModalProps) => {
+  if (!article) return null;
+
+  const getSourceColor = (category: string) => {
+    switch (category) {
+      case 'rss': return 'bg-blue-500/10 text-blue-700 border-blue-200';
+      case 'youtube': return 'bg-red-500/10 text-red-700 border-red-200';
+      case 'steam': return 'bg-gray-500/10 text-gray-700 border-gray-200';
+      case 'actualites': return 'bg-green-500/10 text-green-700 border-green-200';
+      default: return 'bg-muted text-muted-foreground';
+    }
+  };
+
+  const getYouTubeVideoId = (url: string) => {
+    const regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/;
+    const match = url.match(regex);
+    return match ? match[1] : null;
+  };
+
+  const isYouTubeVideo = article.category === 'youtube' && article.url;
+  const youtubeVideoId = isYouTubeVideo ? getYouTubeVideoId(article.url!) : null;
+
+  return (
+    <Dialog open={isOpen} onOpenChange={onClose}>
+      <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+        <DialogHeader className="space-y-4">
+          <div className="flex items-center gap-2">
+            <Badge variant="outline" className={getSourceColor(article.category)}>
+              {article.source}
+            </Badge>
+            <div className="flex items-center gap-1 text-xs text-muted-foreground">
+              <Clock className="h-3 w-3" />
+              {article.readTime} min
+            </div>
+          </div>
+          
+          <DialogTitle className="text-xl font-bold leading-tight text-left">
+            {article.title}
+          </DialogTitle>
+          
+          <div className="flex items-center gap-4 text-sm text-muted-foreground">
+            <div className="flex items-center gap-1">
+              <Calendar className="h-4 w-4" />
+              {new Date(article.publishedAt).toLocaleDateString('fr-FR', {
+                day: 'numeric',
+                month: 'long',
+                year: 'numeric',
+                hour: '2-digit',
+                minute: '2-digit'
+              })}
+            </div>
+          </div>
+        </DialogHeader>
+
+        <div className="space-y-6">
+          {/* YouTube Video Player */}
+          {isYouTubeVideo && youtubeVideoId && (
+            <div className="aspect-video w-full">
+              <iframe
+                src={`https://www.youtube.com/embed/${youtubeVideoId}`}
+                title={article.title}
+                className="w-full h-full rounded-lg"
+                allowFullScreen
+                allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+              />
+            </div>
+          )}
+
+          {/* Article Image */}
+          {article.imageUrl && !isYouTubeVideo && (
+            <div className="w-full">
+              <img 
+                src={article.imageUrl} 
+                alt={article.title}
+                className="w-full h-auto rounded-lg object-cover"
+              />
+            </div>
+          )}
+
+          {/* Article Description */}
+          <div className="prose prose-sm max-w-none">
+            <p className="text-muted-foreground leading-relaxed">
+              {article.description}
+            </p>
+          </div>
+
+          {/* Article Content */}
+          <div className="prose prose-sm max-w-none">
+            <div className="text-foreground leading-relaxed whitespace-pre-wrap">
+              {article.content}
+            </div>
+          </div>
+
+          {/* External Link Button */}
+          {article.url && (
+            <div className="flex justify-end pt-4 border-t">
+              <Button 
+                variant="outline" 
+                className="gap-2" 
+                onClick={() => window.open(article.url, '_blank')}
+              >
+                <ExternalLink className="h-4 w-4" />
+                Voir la source
+              </Button>
+            </div>
+          )}
+        </div>
+      </DialogContent>
+    </Dialog>
+  );
+};
+
+export default ArticleModal;

+ 33 - 8
src/components/NewsCard.tsx

@@ -17,9 +17,10 @@ interface NewsCardProps {
   onTogglePin: (id: string) => void;
   onMarkAsRead: (id: string) => void;
   onDelete: (id: string) => void;
+  onOpenArticle: (article: NewsItem) => void;
 }
 
-const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps) => {
+const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete, onOpenArticle }: NewsCardProps) => {
   const getSourceColor = (category: string) => {
     switch (category) {
       case 'rss': return 'bg-blue-500/10 text-blue-700 border-blue-200';
@@ -30,16 +31,23 @@ const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps)
     }
   };
 
+  const handleCardClick = () => {
+    onOpenArticle(news);
+    if (!news.isRead) {
+      onMarkAsRead(news.id);
+    }
+  };
+
   return (
     <Card className={cn(
-      "group hover:shadow-lg transition-all duration-300 border-l-4",
+      "group hover:shadow-lg transition-all duration-300 border-l-4 cursor-pointer",
       news.isPinned && "border-l-yellow-500",
       news.isRead && "opacity-75",
       !news.isRead && "border-l-primary"
     )}>
       <CardHeader className="space-y-3">
         <div className="flex items-start justify-between gap-4">
-          <div className="flex-1 space-y-2">
+          <div className="flex-1 space-y-2" onClick={handleCardClick}>
             <div className="flex items-center gap-2">
               <Badge variant="outline" className={getSourceColor(news.category)}>
                 {news.source}
@@ -62,7 +70,10 @@ const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps)
             <Button
               variant="ghost"
               size="sm"
-              onClick={() => onTogglePin(news.id)}
+              onClick={(e) => {
+                e.stopPropagation();
+                onTogglePin(news.id);
+              }}
               className={cn(
                 "h-8 w-8 p-0",
                 news.isPinned && "text-yellow-600"
@@ -74,7 +85,10 @@ const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps)
             <Button
               variant="ghost"
               size="sm"
-              onClick={() => onDelete(news.id)}
+              onClick={(e) => {
+                e.stopPropagation();
+                onDelete(news.id);
+              }}
               className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
             >
               <Trash2 className="h-4 w-4" />
@@ -83,7 +97,7 @@ const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps)
         </div>
       </CardHeader>
       
-      <CardContent className="space-y-4">
+      <CardContent className="space-y-4" onClick={handleCardClick}>
         <p className="text-sm text-muted-foreground leading-relaxed">
           {news.description}
         </p>
@@ -103,7 +117,10 @@ const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps)
               <Button
                 variant="outline"
                 size="sm"
-                onClick={() => onMarkAsRead(news.id)}
+                onClick={(e) => {
+                  e.stopPropagation();
+                  onMarkAsRead(news.id);
+                }}
                 className="gap-1"
               >
                 <Eye className="h-3 w-3" />
@@ -112,7 +129,15 @@ const NewsCard = ({ news, onTogglePin, onMarkAsRead, onDelete }: NewsCardProps)
             )}
             
             {news.url && (
-              <Button variant="default" size="sm" className="gap-1">
+              <Button 
+                variant="default" 
+                size="sm" 
+                className="gap-1"
+                onClick={(e) => {
+                  e.stopPropagation();
+                  window.open(news.url, '_blank');
+                }}
+              >
                 <ExternalLink className="h-3 w-3" />
                 Lire
               </Button>

+ 22 - 1
src/pages/Index.tsx

@@ -1,12 +1,13 @@
-
 import { useState, useMemo } from 'react';
 import { categories } from '@/data/mockNews';
 import { useArticles } from '@/hooks/useArticles';
 import { useAuth } from '@/hooks/useAuth';
+import { NewsItem } from '@/types/news';
 import Header from '@/components/Header';
 import CategoryFilter from '@/components/CategoryFilter';
 import NewsCard from '@/components/NewsCard';
 import AddFeedModal from '@/components/AddFeedModal';
+import ArticleModal from '@/components/ArticleModal';
 import { Badge } from '@/components/ui/badge';
 import { Button } from '@/components/ui/button';
 import { Card, CardContent } from '@/components/ui/card';
@@ -21,6 +22,8 @@ const Index = () => {
   const [searchQuery, setSearchQuery] = useState('');
   const [showFilters, setShowFilters] = useState(true);
   const [isAddFeedModalOpen, setIsAddFeedModalOpen] = useState(false);
+  const [selectedArticle, setSelectedArticle] = useState<NewsItem | null>(null);
+  const [isArticleModalOpen, setIsArticleModalOpen] = useState(false);
 
   const filteredNews = useMemo(() => {
     let filtered = articles;
@@ -60,6 +63,16 @@ const Index = () => {
     toast.success(`Flux "${feedData.name}" ajouté avec succès!`);
   };
 
+  const handleOpenArticle = (article: NewsItem) => {
+    setSelectedArticle(article);
+    setIsArticleModalOpen(true);
+  };
+
+  const handleCloseArticleModal = () => {
+    setIsArticleModalOpen(false);
+    setSelectedArticle(null);
+  };
+
   if (loading) {
     return (
       <div className="min-h-screen bg-background flex items-center justify-center">
@@ -205,6 +218,7 @@ const Index = () => {
                     onTogglePin={togglePin}
                     onMarkAsRead={markAsRead}
                     onDelete={deleteArticle}
+                    onOpenArticle={handleOpenArticle}
                   />
                 ))}
               </div>
@@ -213,6 +227,7 @@ const Index = () => {
         </div>
       </main>
 
+      {/* Modals */}
       {user && (
         <AddFeedModal 
           isOpen={isAddFeedModalOpen}
@@ -221,6 +236,12 @@ const Index = () => {
           categories={categories}
         />
       )}
+
+      <ArticleModal 
+        isOpen={isArticleModalOpen}
+        onClose={handleCloseArticleModal}
+        article={selectedArticle}
+      />
     </div>
   );
 };