NewsCard.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import { NewsItem } from '@/types/news';
  2. import { Card, CardContent, CardHeader } from '@/components/ui/card';
  3. import { Badge } from '@/components/ui/badge';
  4. import { Button } from '@/components/ui/button';
  5. import { Clock, Pin, ExternalLink, Eye, Trash2 } from 'lucide-react';
  6. import { cn } from '@/lib/utils';
  7. import { useAuth } from '@/hooks/useAuth';
  8. interface NewsCardProps {
  9. news: NewsItem;
  10. onTogglePin: (id: string) => void;
  11. onMarkAsRead: (id: string) => void;
  12. onDelete: (id: string) => void;
  13. onOpenArticle: (article: NewsItem) => void;
  14. }
  15. const NewsCard = ({
  16. news,
  17. onTogglePin,
  18. onMarkAsRead,
  19. onDelete,
  20. onOpenArticle
  21. }: NewsCardProps) => {
  22. const { user } = useAuth();
  23. // Function to decode HTML entities
  24. const decodeHtmlEntities = (text: string) => {
  25. const textarea = document.createElement('textarea');
  26. textarea.innerHTML = text;
  27. return textarea.value;
  28. };
  29. const getSourceColor = (category: string) => {
  30. switch (category) {
  31. case 'rss':
  32. return 'bg-blue-500/10 text-blue-700 border-blue-200';
  33. case 'youtube':
  34. return 'bg-red-500/10 text-red-700 border-red-200';
  35. case 'steam':
  36. return 'bg-gray-500/10 text-gray-700 border-gray-200';
  37. case 'actualites':
  38. return 'bg-green-500/10 text-green-700 border-green-200';
  39. default:
  40. return 'bg-muted text-muted-foreground';
  41. }
  42. };
  43. const getYouTubeVideoId = (url: string) => {
  44. const regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/;
  45. const match = url.match(regex);
  46. return match ? match[1] : null;
  47. };
  48. const getYouTubeThumbnail = (url: string) => {
  49. const videoId = getYouTubeVideoId(url);
  50. return videoId ? `https://img.youtube.com/vi/${videoId}/hqdefault.jpg` : null;
  51. };
  52. const handleCardClick = () => {
  53. onOpenArticle(news);
  54. // Don't automatically mark as read on card click - user can use the "Mark as read" button
  55. };
  56. return (
  57. <Card className={cn(
  58. "group hover:shadow-lg transition-all duration-300 border-l-4 cursor-pointer",
  59. news.isPinned && "border-l-yellow-500",
  60. news.isRead && "opacity-75",
  61. !news.isRead && "border-l-primary"
  62. )}>
  63. <CardHeader className="space-y-3">
  64. <div className="flex items-start justify-between gap-4">
  65. <div className="flex-1 space-y-2" onClick={handleCardClick}>
  66. <div className="flex items-center gap-2">
  67. <Badge variant="outline" className={getSourceColor(news.category)}>
  68. {news.source}
  69. </Badge>
  70. </div>
  71. <h3 className={cn(
  72. "font-semibold leading-tight group-hover:text-primary transition-colors",
  73. news.isRead && "text-muted-foreground"
  74. )}>
  75. {decodeHtmlEntities(news.title)}
  76. </h3>
  77. </div>
  78. <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
  79. <Button
  80. variant="ghost"
  81. size="sm"
  82. onClick={(e) => {
  83. e.stopPropagation();
  84. onTogglePin(news.id);
  85. }}
  86. disabled={!user}
  87. className={cn(
  88. "h-8 w-8 p-0",
  89. news.isPinned && "text-yellow-600",
  90. !user && "opacity-50 cursor-not-allowed"
  91. )}
  92. >
  93. <Pin className={cn("h-4 w-4", news.isPinned && "fill-current")} />
  94. </Button>
  95. </div>
  96. </div>
  97. </CardHeader>
  98. <CardContent className="space-y-4" onClick={handleCardClick}>
  99. <div className="space-y-3">
  100. {/* Show image only for non-YouTube articles */}
  101. {news.imageUrl && news.category !== 'youtube' && (
  102. <div className="w-full">
  103. <img
  104. src={news.imageUrl}
  105. alt={news.title}
  106. className="w-full h-48 object-cover rounded-md"
  107. />
  108. </div>
  109. )}
  110. {news.category !== 'youtube' && (
  111. <div className="space-y-4">
  112. <p className="text-sm text-muted-foreground leading-relaxed">
  113. {decodeHtmlEntities(news.description)}
  114. </p>
  115. </div>
  116. )}
  117. </div>
  118. <div className="flex items-center justify-between pt-2">
  119. <span className="text-xs text-muted-foreground">
  120. {new Date(news.publishedAt).toLocaleDateString('fr-FR', {
  121. day: 'numeric',
  122. month: 'long',
  123. hour: '2-digit',
  124. minute: '2-digit'
  125. })}
  126. </span>
  127. <div className="flex items-center gap-2">
  128. {!news.isRead && user && (
  129. <Button
  130. variant="outline"
  131. size="sm"
  132. onClick={(e) => {
  133. e.stopPropagation();
  134. onMarkAsRead(news.id);
  135. }}
  136. className="gap-1"
  137. >
  138. <Eye className="h-3 w-3" />
  139. Marquer lu
  140. </Button>
  141. )}
  142. {news.url && (
  143. <Button
  144. variant="default"
  145. size="sm"
  146. className="gap-1"
  147. onClick={(e) => {
  148. e.stopPropagation();
  149. window.open(news.url, '_blank');
  150. }}
  151. >
  152. <ExternalLink className="h-3 w-3" />
  153. Lire
  154. </Button>
  155. )}
  156. </div>
  157. </div>
  158. </CardContent>
  159. </Card>
  160. );
  161. };
  162. export default NewsCard;