Index.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import { useState, useMemo, useEffect } from 'react';
  2. import { categories } from '@/data/mockNews';
  3. import { useRealArticles } from '@/hooks/useRealArticles';
  4. import { useAuth } from '@/hooks/useAuth';
  5. import { useIsMobile } from '@/hooks/use-mobile';
  6. import { NewsItem } from '@/types/news';
  7. import Header from '@/components/Header';
  8. import CategoryFilter from '@/components/CategoryFilter';
  9. import NewsCard from '@/components/NewsCard';
  10. import AddFeedModal from '@/components/AddFeedModal';
  11. import ArticleModal from '@/components/ArticleModal';
  12. import { Badge } from '@/components/ui/badge';
  13. import { Button } from '@/components/ui/button';
  14. import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PaginationEllipsis } from '@/components/ui/pagination';
  15. import { RefreshCw, Filter, Rss, Plus } from 'lucide-react';
  16. import { toast } from 'sonner';
  17. import { Link, useNavigate } from 'react-router-dom';
  18. const ARTICLES_PER_PAGE = 20;
  19. const Index = () => {
  20. const {
  21. user
  22. } = useAuth();
  23. const navigate = useNavigate();
  24. const [dateFilter, setDateFilter] = useState<'today' | 'yesterday' | null>(null);
  25. const [showFollowedOnly, setShowFollowedOnly] = useState(!!user);
  26. const [showDiscoveryMode, setShowDiscoveryMode] = useState(false);
  27. const [showReadArticles, setShowReadArticles] = useState(false);
  28. // Handle view mode changes (followed, discovery, all)
  29. const handleViewModeChange = (mode: 'followed' | 'discovery' | 'all') => {
  30. switch (mode) {
  31. case 'followed':
  32. setShowFollowedOnly(true);
  33. setShowDiscoveryMode(false);
  34. break;
  35. case 'discovery':
  36. setShowFollowedOnly(false);
  37. setShowDiscoveryMode(true);
  38. setDateFilter(null);
  39. break;
  40. case 'all':
  41. setShowFollowedOnly(false);
  42. setShowDiscoveryMode(false);
  43. setDateFilter(null);
  44. break;
  45. }
  46. };
  47. const {
  48. articles,
  49. loading,
  50. togglePin,
  51. markAsRead,
  52. deleteArticle,
  53. refetch
  54. } = useRealArticles(dateFilter, showFollowedOnly, showReadArticles, showDiscoveryMode);
  55. const isMobile = useIsMobile();
  56. const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
  57. const [searchQuery, setSearchQuery] = useState('');
  58. const [showFilters, setShowFilters] = useState(true);
  59. const [isAddFeedModalOpen, setIsAddFeedModalOpen] = useState(false);
  60. const [selectedArticle, setSelectedArticle] = useState<NewsItem | null>(null);
  61. const [isArticleModalOpen, setIsArticleModalOpen] = useState(false);
  62. const [currentPage, setCurrentPage] = useState(1);
  63. console.log('🏠 Index page - Articles count:', articles.length, 'Loading:', loading, 'User:', !!user);
  64. const filteredNews = useMemo(() => {
  65. let filtered = articles;
  66. if (selectedCategory) {
  67. const category = categories.find(c => c.id === selectedCategory);
  68. if (category) {
  69. filtered = filtered.filter(item => item.category === category.type);
  70. }
  71. }
  72. if (searchQuery) {
  73. filtered = filtered.filter(item => item.title.toLowerCase().includes(searchQuery.toLowerCase()) || item.description.toLowerCase().includes(searchQuery.toLowerCase()) || item.source.toLowerCase().includes(searchQuery.toLowerCase()));
  74. }
  75. return filtered.sort((a, b) => {
  76. if (a.isPinned && !b.isPinned) return -1;
  77. if (!a.isPinned && b.isPinned) return 1;
  78. return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
  79. });
  80. }, [articles, selectedCategory, searchQuery]);
  81. // Separate pinned and regular articles
  82. const pinnedArticles = useMemo(() => {
  83. return filteredNews.filter(article => article.isPinned);
  84. }, [filteredNews]);
  85. const regularArticles = useMemo(() => {
  86. return filteredNews.filter(article => !article.isPinned);
  87. }, [filteredNews]);
  88. // Pagination logic
  89. const totalPages = Math.ceil(regularArticles.length / ARTICLES_PER_PAGE);
  90. const paginatedArticles = useMemo(() => {
  91. const startIndex = (currentPage - 1) * ARTICLES_PER_PAGE;
  92. return regularArticles.slice(startIndex, startIndex + ARTICLES_PER_PAGE);
  93. }, [regularArticles, currentPage]);
  94. // Reset page when filters change
  95. useEffect(() => {
  96. setCurrentPage(1);
  97. }, [selectedCategory, searchQuery, dateFilter, showFollowedOnly, showDiscoveryMode, showReadArticles]);
  98. const pinnedCount = articles.filter(item => item.isPinned).length;
  99. const unreadCount = articles.filter(item => !item.isRead).length;
  100. // Update document title with unread count
  101. useEffect(() => {
  102. const baseTitle = 'Feeds.Duhaz.fr';
  103. if (unreadCount > 0) {
  104. document.title = `(${unreadCount}) ${baseTitle}`;
  105. } else {
  106. document.title = baseTitle;
  107. }
  108. }, [unreadCount]);
  109. // Auto-refresh articles every 5 minutes, paused when article modal is open
  110. const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
  111. useEffect(() => {
  112. if (isArticleModalOpen) {
  113. return;
  114. }
  115. const intervalId = setInterval(() => {
  116. refetch();
  117. }, REFRESH_INTERVAL);
  118. return () => {
  119. clearInterval(intervalId);
  120. };
  121. }, [isArticleModalOpen, refetch]);
  122. const handleRefresh = () => {
  123. refetch();
  124. toast.success("Flux actualisés");
  125. };
  126. const handleAddFeed = (feedData: any) => {
  127. console.log('Nouveau flux ajouté:', feedData);
  128. toast.success(`Flux "${feedData.name}" ajouté avec succès!`);
  129. };
  130. const handleOpenArticle = (article: NewsItem) => {
  131. setSelectedArticle(article);
  132. setIsArticleModalOpen(true);
  133. };
  134. const handleCloseArticleModal = () => {
  135. setIsArticleModalOpen(false);
  136. setSelectedArticle(null);
  137. };
  138. const handleSourceClick = (feedId: string, feedName: string) => {
  139. navigate(`/feed/${feedId}`);
  140. };
  141. if (loading) {
  142. return <div className="min-h-screen bg-background flex items-center justify-center">
  143. <div className="flex items-center gap-2">
  144. <Rss className="h-6 w-6 animate-spin text-primary" />
  145. <p>Chargement des articles...</p>
  146. </div>
  147. </div>;
  148. }
  149. return <div className="min-h-screen bg-background">
  150. <Header
  151. searchQuery={searchQuery}
  152. onSearchChange={setSearchQuery}
  153. pinnedCount={pinnedCount}
  154. categories={categories}
  155. selectedCategory={selectedCategory}
  156. onCategoryChange={setSelectedCategory}
  157. articles={articles}
  158. pinnedArticles={pinnedArticles}
  159. dateFilter={dateFilter}
  160. onDateFilterChange={setDateFilter}
  161. showFollowedOnly={showFollowedOnly}
  162. showDiscoveryMode={showDiscoveryMode}
  163. onViewModeChange={handleViewModeChange}
  164. showReadArticles={showReadArticles}
  165. onShowReadArticlesChange={setShowReadArticles}
  166. unreadCount={unreadCount}
  167. onTogglePin={togglePin}
  168. onMarkAsRead={markAsRead}
  169. onDeleteArticle={deleteArticle}
  170. onOpenArticle={handleOpenArticle}
  171. />
  172. <main className="container mx-auto px-4 py-6">
  173. {/* Message pour les utilisateurs non connectés */}
  174. {!user && (
  175. <div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
  176. <p className="text-blue-800">
  177. Vous consultez les articles en mode visiteur.
  178. <Link to="/auth" className="font-semibold text-blue-600 hover:text-blue-800 ml-1">
  179. Connectez-vous
  180. </Link> pour gérer vos flux et marquer vos articles préférés.
  181. </p>
  182. </div>
  183. )}
  184. {/* Message pour les utilisateurs connectés sans articles suivis */}
  185. {user && articles.length === 0 && (
  186. <div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
  187. <p className="text-amber-800">
  188. Vous ne suivez aucun flux RSS pour le moment.
  189. <Link to="/feeds" className="font-semibold text-amber-600 hover:text-amber-800 ml-1">
  190. Ajoutez des flux
  191. </Link> pour commencer à voir des articles.
  192. </p>
  193. </div>
  194. )}
  195. <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
  196. {/* Sidebar - Desktop only */}
  197. {!isMobile && (
  198. <div className={`lg:col-span-1 space-y-6 ${!showFilters && 'hidden lg:block'}`}>
  199. <CategoryFilter
  200. categories={categories}
  201. selectedCategory={selectedCategory}
  202. onCategoryChange={setSelectedCategory}
  203. newsCount={articles.length}
  204. pinnedCount={pinnedCount}
  205. articles={articles}
  206. pinnedArticles={pinnedArticles}
  207. dateFilter={dateFilter}
  208. onDateFilterChange={setDateFilter}
  209. showFollowedOnly={showFollowedOnly}
  210. showDiscoveryMode={showDiscoveryMode}
  211. onViewModeChange={handleViewModeChange}
  212. showReadArticles={showReadArticles}
  213. onShowReadArticlesChange={setShowReadArticles}
  214. onTogglePin={togglePin}
  215. onMarkAsRead={markAsRead}
  216. onDeleteArticle={deleteArticle}
  217. onOpenArticle={handleOpenArticle}
  218. />
  219. <div className="bg-card border rounded-lg p-4 space-y-3">
  220. <h3 className="font-semibold text-sm">Statistiques</h3>
  221. <div className="space-y-2 text-sm">
  222. <div className="flex justify-between">
  223. <span className="text-muted-foreground">Articles non lus</span>
  224. <Badge variant="outline">{unreadCount}</Badge>
  225. </div>
  226. <div className="flex justify-between">
  227. <span className="text-muted-foreground">Articles totaux</span>
  228. <Badge variant="outline">{articles.length}</Badge>
  229. </div>
  230. {user && <div className="flex justify-between">
  231. <span className="text-muted-foreground">Épinglés</span>
  232. <Badge variant="secondary">{pinnedCount}</Badge>
  233. </div>}
  234. </div>
  235. </div>
  236. </div>
  237. )}
  238. {/* Main content */}
  239. <div className={`${isMobile ? 'col-span-1' : 'lg:col-span-3'} space-y-6`}>
  240. <div className="flex items-center justify-between">
  241. <div className="flex items-center gap-4">
  242. <h2 className="text-2xl font-bold">
  243. {showReadArticles ? 'Tous les articles' : (user ? 'Articles non lus' : 'Derniers articles')}
  244. </h2>
  245. </div>
  246. <div className="flex items-center gap-2">
  247. {/* Desktop Filter Toggle - keep existing logic */}
  248. {!isMobile && (
  249. <Button variant="outline" size="sm" onClick={() => setShowFilters(!showFilters)} className="lg:hidden gap-2">
  250. <Filter className="h-4 w-4" />
  251. Filtres
  252. </Button>
  253. )}
  254. <Button variant="outline" size="sm" onClick={handleRefresh} className="gap-2">
  255. <RefreshCw className="h-4 w-4" />
  256. Actualiser
  257. </Button>
  258. </div>
  259. </div>
  260. {regularArticles.length === 0 && articles.length > 0 ? <div className="text-center py-12">
  261. <p className="text-muted-foreground text-lg">Aucun article trouvé avec ces filtres</p>
  262. <p className="text-sm text-muted-foreground mt-2">
  263. Essayez de modifier vos filtres ou votre recherche
  264. </p>
  265. {pinnedArticles.length > 0 && (
  266. <p className="text-xs text-muted-foreground mt-2">
  267. ({pinnedArticles.length} article{pinnedArticles.length > 1 ? 's' : ''} épinglé{pinnedArticles.length > 1 ? 's' : ''} dans la sidebar)
  268. </p>
  269. )}
  270. </div> : regularArticles.length === 0 ? <div className="text-center py-12">
  271. <Rss className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
  272. <p className="text-muted-foreground text-lg">Aucun article non lu disponible</p>
  273. <p className="text-sm text-muted-foreground mt-2 mb-4">
  274. {user ? 'Bravo ! Tous vos articles sont lus ou suivez des flux RSS pour voir des articles ici' : 'Aucun article public disponible pour le moment'}
  275. </p>
  276. {pinnedArticles.length > 0 && (
  277. <p className="text-xs text-muted-foreground mb-4">
  278. ({pinnedArticles.length} article{pinnedArticles.length > 1 ? 's' : ''} épinglé{pinnedArticles.length > 1 ? 's' : ''} dans la sidebar)
  279. </p>
  280. )}
  281. {user && <div className="flex gap-2 justify-center">
  282. <Link to="/feeds">
  283. <Button variant="outline">
  284. Gérer les flux
  285. </Button>
  286. </Link>
  287. <Button onClick={() => setIsAddFeedModalOpen(true)}>
  288. <Plus className="h-4 w-4 mr-2" />
  289. Ajouter un flux
  290. </Button>
  291. </div>}
  292. </div> : <div className="space-y-4">
  293. {paginatedArticles.map(item => <NewsCard key={item.id} news={item} onTogglePin={togglePin} onMarkAsRead={markAsRead} onDelete={deleteArticle} onOpenArticle={handleOpenArticle} onSourceClick={handleSourceClick} isDiscoveryMode={showDiscoveryMode} />)}
  294. {/* Pagination */}
  295. {totalPages > 1 && (
  296. <Pagination className="mt-8">
  297. <PaginationContent>
  298. <PaginationItem>
  299. <PaginationPrevious
  300. onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
  301. className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
  302. />
  303. </PaginationItem>
  304. {Array.from({ length: totalPages }, (_, i) => i + 1)
  305. .filter(page => page === 1 || page === totalPages || Math.abs(page - currentPage) <= 1)
  306. .map((page, index, array) => (
  307. <>
  308. {index > 0 && array[index - 1] !== page - 1 && (
  309. <PaginationItem key={`ellipsis-${page}`}>
  310. <PaginationEllipsis />
  311. </PaginationItem>
  312. )}
  313. <PaginationItem key={page}>
  314. <PaginationLink
  315. onClick={() => setCurrentPage(page)}
  316. isActive={currentPage === page}
  317. className="cursor-pointer"
  318. >
  319. {page}
  320. </PaginationLink>
  321. </PaginationItem>
  322. </>
  323. ))}
  324. <PaginationItem>
  325. <PaginationNext
  326. onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
  327. className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
  328. />
  329. </PaginationItem>
  330. </PaginationContent>
  331. </Pagination>
  332. )}
  333. {/* Info pagination */}
  334. {totalPages > 1 && (
  335. <p className="text-center text-sm text-muted-foreground">
  336. Page {currentPage} sur {totalPages} ({regularArticles.length} articles)
  337. </p>
  338. )}
  339. </div>}
  340. </div>
  341. </div>
  342. </main>
  343. {/* Modals */}
  344. {user && <AddFeedModal isOpen={isAddFeedModalOpen} onClose={() => setIsAddFeedModalOpen(false)} onAddFeed={handleAddFeed} categories={categories} />}
  345. <ArticleModal isOpen={isArticleModalOpen} onClose={handleCloseArticleModal} article={selectedArticle} />
  346. </div>;
  347. };
  348. export default Index;