Переглянути джерело

Optimise performance and typings

Refactor to remove debug logs, add memoization, and improve TS types:
- Remove or minimize console.log usage in production paths
- Introduce memoized HTML entity decoding utility and reuse in NewsCard
- Memoize category counts in CategoryFilter and fix typing from any[] to NewsItem[]
- Implement minor typing and safety improvements across related components

X-Lovable-Edit-ID: edt-2ebc1c27-2dbb-4f2a-963a-021cc2ac37d8
gpt-engineer-app[bot] 4 днів тому
батько
коміт
9f8a5b47aa

+ 14 - 14
src/components/CategoryFilter.tsx

@@ -1,5 +1,6 @@
-
-import { NewsCategory } from '@/types/news';
+import { useMemo, useState } from 'react';
+import { Link } from 'react-router-dom';
+import { NewsCategory, NewsItem } from '@/types/news';
 import { Button } from '@/components/ui/button';
 import { Badge } from '@/components/ui/badge';
 import { cn } from '@/lib/utils';
@@ -21,9 +22,7 @@ import {
   ArrowRight
 } from 'lucide-react';
 import { useAuth } from '@/hooks/useAuth';
-import { NewsItem } from '@/types/news';
 import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
-import { ScrollArea } from '@/components/ui/scroll-area';
 import { 
   AlertDialog,
   AlertDialogAction,
@@ -35,8 +34,6 @@ import {
   AlertDialogTitle,
   AlertDialogTrigger,
 } from '@/components/ui/alert-dialog';
-import { useState } from 'react';
-import { Link } from 'react-router-dom';
 
 interface CategoryFilterProps {
   categories: NewsCategory[];
@@ -44,8 +41,8 @@ interface CategoryFilterProps {
   onCategoryChange: (categoryId: string | null) => void;
   newsCount: number;
   pinnedCount?: number;
-  articles: any[]; // Add articles to calculate counts per category
-  pinnedArticles?: NewsItem[]; // Pinned articles to display
+  articles: NewsItem[];
+  pinnedArticles?: NewsItem[];
   dateFilter?: 'today' | 'yesterday' | null;
   onDateFilterChange?: (filter: 'today' | 'yesterday' | null) => void;
   showFollowedOnly?: boolean;
@@ -89,10 +86,14 @@ const CategoryFilter = ({
   const { user } = useAuth();
   const [isPinnedExpanded, setIsPinnedExpanded] = useState(true);
 
-  // Calculate count for each category
-  const getCategoryCount = (categoryType: string) => {
-    return articles.filter(article => article.category === categoryType).length;
-  };
+  // Memoize category counts to avoid recalculation on every render
+  const categoryCounts = useMemo(() => {
+    const counts: Record<string, number> = {};
+    for (const article of articles) {
+      counts[article.category] = (counts[article.category] || 0) + 1;
+    }
+    return counts;
+  }, [articles]);
 
   return (
     <div className="bg-card border rounded-lg p-6 space-y-4">
@@ -116,7 +117,7 @@ const CategoryFilter = ({
         {categories.map((category) => {
           const IconComponent = iconMap[category.icon as keyof typeof iconMap];
           const isSelected = selectedCategory === category.id;
-          const categoryCount = getCategoryCount(category.type);
+          const categoryCount = categoryCounts[category.type] || 0;
           
           return (
             <Button
@@ -383,7 +384,6 @@ const CategoryFilter = ({
           </div>
         </div>
       )}
-
     </div>
   );
 };

+ 110 - 83
src/components/NewsCard.tsx

@@ -5,7 +5,8 @@ import { Button } from '@/components/ui/button';
 import { Clock, Pin, ExternalLink, Eye, Trash2, Copy, Rss, Youtube, Gamepad2, Newspaper } from 'lucide-react';
 import { cn } from '@/lib/utils';
 import { useAuth } from '@/hooks/useAuth';
-import { toast } from 'sonner';
+import { decodeHtmlEntities } from '@/utils/htmlDecode';
+
 interface NewsCardProps {
   news: NewsItem;
   onTogglePin: (id: string) => void;
@@ -15,6 +16,37 @@ interface NewsCardProps {
   onSourceClick?: (feedId: string, feedName: string) => void;
   isDiscoveryMode?: boolean;
 }
+
+const getCategoryIcon = (category: string) => {
+  switch (category) {
+    case 'rss':
+      return <Rss className="h-4 w-4 text-blue-600" />;
+    case 'youtube':
+      return <Youtube className="h-4 w-4 text-red-600" />;
+    case 'steam':
+      return <Gamepad2 className="h-4 w-4 text-gray-600" />;
+    case 'actualites':
+      return <Newspaper className="h-4 w-4 text-green-600" />;
+    default:
+      return <Rss className="h-4 w-4 text-muted-foreground" />;
+  }
+};
+
+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 NewsCard = ({
   news,
   onTogglePin,
@@ -24,58 +56,20 @@ const NewsCard = ({
   onSourceClick,
   isDiscoveryMode
 }: NewsCardProps) => {
-  const {
-    user
-  } = useAuth();
+  const { user } = useAuth();
 
-  // Function to decode HTML entities
-  const decodeHtmlEntities = (text: string) => {
-    const textarea = document.createElement('textarea');
-    textarea.innerHTML = text;
-    return textarea.value;
-  };
-  const getCategoryIcon = (category: string) => {
-    switch (category) {
-      case 'rss':
-        return <Rss className="h-4 w-4 text-blue-600" />;
-      case 'youtube':
-        return <Youtube className="h-4 w-4 text-red-600" />;
-      case 'steam':
-        return <Gamepad2 className="h-4 w-4 text-gray-600" />;
-      case 'actualites':
-        return <Newspaper className="h-4 w-4 text-green-600" />;
-      default:
-        return <Rss className="h-4 w-4 text-muted-foreground" />;
-    }
-  };
-  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 getYouTubeThumbnail = (url: string) => {
-    const videoId = getYouTubeVideoId(url);
-    return videoId ? `https://img.youtube.com/vi/${videoId}/hqdefault.jpg` : null;
-  };
   const handleCardClick = () => {
     onOpenArticle(news);
-    // Don't automatically mark as read on card click - user can use the "Mark as read" button
   };
-  return <Card className={cn("group hover:shadow-lg transition-all duration-300 border-l-4 cursor-pointer", news.isPinned && "border-l-yellow-500", isDiscoveryMode && "border-l-purple-500", news.isRead && "opacity-75", !news.isRead && !isDiscoveryMode && "border-l-primary")}>
+
+  return (
+    <Card className={cn(
+      "group hover:shadow-lg transition-all duration-300 border-l-4 cursor-pointer",
+      news.isPinned && "border-l-yellow-500",
+      isDiscoveryMode && "border-l-purple-500",
+      news.isRead && "opacity-75",
+      !news.isRead && !isDiscoveryMode && "border-l-primary"
+    )}>
       <CardHeader className="space-y-3">
         <div className="flex items-start justify-between gap-4">
           <div className="flex-1 space-y-2" onClick={handleCardClick}>
@@ -87,17 +81,30 @@ const NewsCard = ({
               )}
             </div>
             
-            <h3 className={cn("flex items-center gap-2 font-semibold leading-tight group-hover:text-primary transition-colors", news.isRead && "text-muted-foreground")}>
+            <h3 className={cn(
+              "flex items-center gap-2 font-semibold leading-tight group-hover:text-primary transition-colors",
+              news.isRead && "text-muted-foreground"
+            )}>
               {getCategoryIcon(news.category)}
               {decodeHtmlEntities(news.title)}
             </h3>
           </div>
           
           <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
-            <Button variant="ghost" size="sm" onClick={e => {
-            e.stopPropagation();
-            onTogglePin(news.id);
-          }} disabled={!user} className={cn("h-8 w-8 p-0", news.isPinned && "text-yellow-600", !user && "opacity-50 cursor-not-allowed")}>
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={e => {
+                e.stopPropagation();
+                onTogglePin(news.id);
+              }}
+              disabled={!user}
+              className={cn(
+                "h-8 w-8 p-0",
+                news.isPinned && "text-yellow-600",
+                !user && "opacity-50 cursor-not-allowed"
+              )}
+            >
               <Pin className={cn("h-4 w-4", news.isPinned && "fill-current")} />
             </Button>
           </div>
@@ -106,16 +113,24 @@ const NewsCard = ({
       
       <CardContent className="space-y-4" onClick={handleCardClick}>
         <div className="space-y-3">
-          {/* Show image only for non-YouTube articles */}
-          {news.imageUrl && news.category !== 'youtube' && <div className="w-full">
-              <img src={news.imageUrl} alt={news.title} className="w-full h-48 object-cover rounded-md" />
-            </div>}
+          {news.imageUrl && news.category !== 'youtube' && (
+            <div className="w-full">
+              <img
+                src={news.imageUrl}
+                alt={news.title}
+                className="w-full h-48 object-cover rounded-md"
+                loading="lazy"
+              />
+            </div>
+          )}
           
-          {news.category !== 'youtube' && <div className="space-y-4">
+          {news.category !== 'youtube' && (
+            <div className="space-y-4">
               <p className="text-sm text-muted-foreground leading-relaxed">
                 {decodeHtmlEntities(news.description)}
               </p>
-            </div>}
+            </div>
+          )}
         </div>
         
         <div className="flex items-center justify-between pt-2">
@@ -137,37 +152,49 @@ const NewsCard = ({
             </Badge>
             <span>
               {new Date(news.publishedAt).toLocaleDateString('fr-FR', {
-              day: 'numeric',
-              month: 'long',
-              hour: '2-digit',
-              minute: '2-digit'
-            })}
+                day: 'numeric',
+                month: 'long',
+                hour: '2-digit',
+                minute: '2-digit'
+              })}
             </span>
           </div>
           
           <div className="flex items-center gap-2">
-            {!news.isRead && user && <Button variant="outline" size="sm" onClick={e => {
-            e.stopPropagation();
-            onMarkAsRead(news.id);
-          }} className="gap-1">
+            {!news.isRead && user && (
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={e => {
+                  e.stopPropagation();
+                  onMarkAsRead(news.id);
+                }}
+                className="gap-1"
+              >
                 <Eye className="h-3 w-3" />
                 Marquer lu
-              </Button>}
+              </Button>
+            )}
             
-            {news.url && <>
-                
-                
-                <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>
-              </>}
+            {news.url && (
+              <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>
+            )}
           </div>
         </div>
       </CardContent>
-    </Card>;
+    </Card>
+  );
 };
-export default NewsCard;
+
+export default NewsCard;

+ 20 - 88
src/hooks/useRealArticles.tsx

@@ -1,10 +1,11 @@
-
 import { useState, useEffect } from 'react';
 import { supabase } from '@/integrations/supabase/client';
 import { useAuth } from './useAuth';
 import { NewsItem } from '@/types/news';
 import { toast } from 'sonner';
 
+const isDev = import.meta.env.DEV;
+
 export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showFollowedOnly?: boolean, showReadArticles?: boolean, showDiscoveryMode?: boolean) {
   const [articles, setArticles] = useState<NewsItem[]>([]);
   const [loading, setLoading] = useState(true);
@@ -13,7 +14,7 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
   const fetchArticles = async () => {
     try {
       setLoading(true);
-      console.log('🔄 Fetching articles...', { user: !!user, dateFilter, showFollowedOnly });
+      if (isDev) console.log('🔄 Fetching articles...', { user: !!user, dateFilter, showFollowedOnly });
       
       // Calculate date ranges for filtering
       let dateStart = null;
@@ -43,16 +44,14 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           .eq('is_followed', true);
 
         if (userFeedsError) {
-          console.error('❌ Error fetching user feeds:', userFeedsError);
+          if (isDev) console.error('❌ Error fetching user feeds:', userFeedsError);
           toast.error('Erreur lors du chargement de vos flux');
           return;
         }
 
-        console.log('📋 User followed feeds:', userFeeds);
         const followedFeedIds = userFeeds?.map(uf => uf.feed_id) || [];
         
         if (followedFeedIds.length === 0) {
-          console.log('⚠️ No followed feeds found for user');
           setArticles([]);
           return;
         }
@@ -75,7 +74,7 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           .order('published_at', { ascending: false })
           .limit(100);
 
-        if (pinnedError) {
+        if (pinnedError && isDev) {
           console.error('❌ Error fetching pinned articles:', pinnedError);
         }
 
@@ -83,7 +82,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
         let regularQuery;
         
         if (!showReadArticles) {
-          // Optimize: exclude read articles at SQL level
           regularQuery = supabase
             .from('articles')
             .select(`
@@ -95,7 +93,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
             .or(`user_articles.is.null,user_articles.is_read.eq.false`)
             .eq('feeds.status', 'active');
         } else {
-          // Include all articles (read and unread)
           regularQuery = supabase
             .from('articles')
             .select(`
@@ -107,7 +104,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
             .eq('feeds.status', 'active');
         }
         
-        // Apply date filter to regular articles only
         if (dateStart && dateEnd) {
           regularQuery = regularQuery.gte('published_at', dateStart).lte('published_at', dateEnd);
         }
@@ -117,7 +113,7 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           .limit(200);
 
         if (regularError) {
-          console.error('❌ Error fetching regular articles:', regularError);
+          if (isDev) console.error('❌ Error fetching regular articles:', regularError);
           toast.error('Erreur lors du chargement des articles');
           return;
         }
@@ -128,15 +124,7 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           index === self.findIndex(a => a.id === article.id)
         );
 
-        console.log('📰 Articles found (SQL filtered for read articles):', {
-          pinned: pinnedArticles?.length || 0,
-          regular: regularArticles?.length || 0,
-          unique: uniqueArticles.length,
-          showReadArticles,
-          sqlFiltered: !showReadArticles
-        });
-
-        // Transform to NewsItem format (read articles already filtered at SQL level when showReadArticles=false)
+        // Transform to NewsItem format
         const transformedArticles: NewsItem[] = uniqueArticles
           ?.map(article => ({
             id: article.id,
@@ -157,7 +145,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
         setArticles(transformedArticles.slice(0, 100));
       } else if (showDiscoveryMode && user) {
         // ======= MODE DÉCOUVERTE =======
-        console.log('🔍 Discovery mode active');
         
         // 1. Récupérer tous les feed_id que l'utilisateur a déjà interagi avec
         const { data: knownFeeds } = await supabase
@@ -166,7 +153,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           .eq('user_id', user.id);
         
         const knownFeedIds = knownFeeds?.map(f => f.feed_id) || [];
-        console.log('📚 Known feeds to exclude:', knownFeedIds);
         
         // 2. Récupérer uniquement les articles des flux inconnus + actifs
         let discoveryQuery = supabase
@@ -178,12 +164,10 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           `)
           .eq('feeds.status', 'active');
         
-        // Exclure les flux connus
         if (knownFeedIds.length > 0) {
           discoveryQuery = discoveryQuery.not('feed_id', 'in', `(${knownFeedIds.join(',')})`);
         }
         
-        // Appliquer filtre "lus/non lus" si l'utilisateur a cliqué sur certains articles découverte
         if (!showReadArticles && user) {
           discoveryQuery = discoveryQuery.or('user_articles.is.null,user_articles.is_read.eq.false', { referencedTable: 'user_articles' });
         }
@@ -195,14 +179,11 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
         const { data: discoveryArticles, error: discoveryError } = await discoveryQuery;
         
         if (discoveryError) {
-          console.error('❌ Error fetching discovery articles:', discoveryError);
+          if (isDev) console.error('❌ Error fetching discovery articles:', discoveryError);
           toast.error('Erreur lors du chargement des articles en découverte');
           return;
         }
         
-        console.log('✅ Discovery articles loaded:', discoveryArticles?.length || 0);
-        
-        // Formater les articles
         const formattedArticles = (discoveryArticles || []).map(article => ({
           id: article.id,
           title: article.title,
@@ -222,14 +203,11 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
         
         setArticles(formattedArticles);
       } else {
-        // For users wanting all articles or visitors - show all articles from all feeds
-        console.log('👤 Loading all articles (visitor or showFollowedOnly=false)');
-        
-        let pinnedArticles = [];
-        let regularArticles = [];
+        // For users wanting all articles or visitors
+        let pinnedArticles: any[] = [];
+        let regularArticles: any[] = [];
 
         if (user) {
-          // Fetch pinned articles first (without date filter) for authenticated users
           const pinnedQuery = supabase
             .from('articles')
             .select(`
@@ -245,14 +223,11 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
             .order('published_at', { ascending: false })
             .limit(100);
 
-          if (pinnedError) {
-            console.error('❌ Error fetching pinned articles:', pinnedError);
-          } else {
+          if (!pinnedError) {
             pinnedArticles = pinnedData || [];
           }
         }
 
-        // Fetch regular articles (NO date filter for "All articles" mode)
         let regularQuery = supabase
           .from('articles')
           .select(`
@@ -262,14 +237,12 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           `)
           .eq('feeds.status', 'active');
         
-        // Don't apply date filter in "All articles" mode - show everything
-        
         const { data: regularData, error: regularError } = await regularQuery
           .order('published_at', { ascending: false })
           .limit(200);
 
         if (regularError) {
-          console.error('❌ Error fetching regular articles:', regularError);
+          if (isDev) console.error('❌ Error fetching regular articles:', regularError);
           toast.error('Erreur lors du chargement des articles');
           return;
         }
@@ -282,27 +255,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
           index === self.findIndex(a => a.id === article.id)
         );
 
-        console.log('📰 All articles found:', {
-          pinned: pinnedArticles.length,
-          regular: regularArticles.length,
-          unique: uniqueArticles.length
-        });
-
-        console.log('🔍 Before filtering - Articles details:', {
-          total: uniqueArticles.length,
-          withFeeds: uniqueArticles.filter(a => a.feeds).length,
-          withUserArticles: uniqueArticles.filter(a => a.user_articles && a.user_articles.length > 0).length,
-          readArticles: uniqueArticles.filter(a => a.user_articles?.[0]?.is_read).length,
-          showReadArticles,
-          sampleArticle: uniqueArticles[0] ? {
-            id: uniqueArticles[0].id,
-            title: uniqueArticles[0].title.substring(0, 50),
-            feeds: !!uniqueArticles[0].feeds,
-            userArticles: uniqueArticles[0].user_articles?.length || 0,
-            isRead: uniqueArticles[0].user_articles?.[0]?.is_read
-          } : null
-        });
-
         // Transform to NewsItem format and conditionally filter read articles
         const transformedArticles: NewsItem[] = uniqueArticles
           ?.filter(article => {
@@ -310,14 +262,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
             const userArticle = article.user_articles?.[0];
             const isRead = userArticle?.is_read || false;
             const shouldShow = showReadArticles || !isRead;
-            
-            if (!hasFeeds) {
-              console.log('❌ Article filtered out - no feeds:', article.id);
-            }
-            if (!shouldShow) {
-              console.log('❌ Article filtered out - read filter:', article.id, { isRead, showReadArticles });
-            }
-            
             return hasFeeds && shouldShow;
           })
           ?.map(article => ({
@@ -336,20 +280,10 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
             feedId: article.feed_id
            })) || [];
 
-        console.log('✅ After filtering - Final articles:', {
-          transformedCount: transformedArticles.length,
-          sampleTransformed: transformedArticles[0] ? {
-            id: transformedArticles[0].id,
-            title: transformedArticles[0].title.substring(0, 50),
-            isRead: transformedArticles[0].isRead,
-            isPinned: transformedArticles[0].isPinned
-           } : null
-        });
-
         setArticles(transformedArticles.slice(0, 100));
       }
     } catch (error) {
-      console.error('💥 Error in fetchArticles:', error);
+      if (isDev) console.error('💥 Error in fetchArticles:', error);
       toast.error('Erreur lors du chargement des articles');
     } finally {
       setLoading(false);
@@ -388,7 +322,7 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
       
       toast.success(article.isPinned ? "Article retiré des épinglés" : "Article épinglé");
     } catch (error) {
-      console.error('Error toggling pin:', error);
+      if (isDev) console.error('Error toggling pin:', error);
       toast.error('Erreur lors de la mise à jour');
     }
   };
@@ -413,11 +347,10 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
         });
 
       if (error) {
-        console.error('Error marking as read:', error);
+        if (isDev) console.error('Error marking as read:', error);
         return;
       }
 
-      // Update local state: remove if not showing read articles, otherwise mark as read
       if (!showReadArticles) {
         setArticles(prev => prev.filter(item => item.id !== articleId));
         toast.success("Article marqué comme lu");
@@ -426,7 +359,7 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
         toast.success("Article marqué comme lu");
       }
     } catch (error) {
-      console.error('Error marking as read:', error);
+      if (isDev) console.error('Error marking as read:', error);
     }
   };
 
@@ -437,7 +370,6 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
     }
 
     try {
-      // Remove from user's view by deleting user_articles record
       const { error } = await supabase
         .from('user_articles')
         .delete()
@@ -452,7 +384,7 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
       setArticles(prev => prev.filter(item => item.id !== articleId));
       toast.success("Article supprimé de votre vue");
     } catch (error) {
-      console.error('Error deleting article:', error);
+      if (isDev) console.error('Error deleting article:', error);
       toast.error('Erreur lors de la suppression');
     }
   };
@@ -471,12 +403,12 @@ export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showF
 
       if (data.success) {
         toast.success(`${data.articlesProcessed} articles récupérés`);
-        await fetchArticles(); // Refresh articles
+        await fetchArticles();
       } else {
         throw new Error(data.error || 'Erreur lors de la récupération RSS');
       }
     } catch (error) {
-      console.error('Error fetching RSS:', error);
+      if (isDev) console.error('Error fetching RSS:', error);
       toast.error('Erreur lors de la récupération du contenu RSS');
     }
   };

+ 35 - 0
src/utils/htmlDecode.ts

@@ -0,0 +1,35 @@
+// Singleton textarea element for HTML entity decoding
+let textareaElement: HTMLTextAreaElement | null = null;
+
+const getTextarea = (): HTMLTextAreaElement => {
+  if (!textareaElement) {
+    textareaElement = document.createElement('textarea');
+  }
+  return textareaElement;
+};
+
+// Simple cache for decoded strings
+const decodeCache = new Map<string, string>();
+const MAX_CACHE_SIZE = 500;
+
+export const decodeHtmlEntities = (text: string): string => {
+  if (!text) return '';
+  
+  // Check cache first
+  const cached = decodeCache.get(text);
+  if (cached !== undefined) return cached;
+  
+  // Decode using singleton textarea
+  const textarea = getTextarea();
+  textarea.innerHTML = text;
+  const decoded = textarea.value;
+  
+  // Cache the result (with size limit)
+  if (decodeCache.size >= MAX_CACHE_SIZE) {
+    const firstKey = decodeCache.keys().next().value;
+    if (firstKey) decodeCache.delete(firstKey);
+  }
+  decodeCache.set(text, decoded);
+  
+  return decoded;
+};