Index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  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 { Card, CardContent } from '@/components/ui/card';
  15. import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
  16. import { RefreshCw, Filter, User, Rss, Plus } from 'lucide-react';
  17. import { toast } from 'sonner';
  18. import { Link } from 'react-router-dom';
  19. const Index = () => {
  20. const {
  21. user
  22. } = useAuth();
  23. const [dateFilter, setDateFilter] = useState<'today' | 'yesterday' | null>(null);
  24. const [showFollowedOnly, setShowFollowedOnly] = useState(false);
  25. const [showReadArticles, setShowReadArticles] = useState(false);
  26. // Reset date filter when switching to "All articles" mode
  27. const handleShowFollowedOnlyChange = (value: boolean) => {
  28. setShowFollowedOnly(value);
  29. if (!value) {
  30. // When switching to "All articles", reset date filter
  31. setDateFilter(null);
  32. }
  33. };
  34. const {
  35. articles,
  36. loading,
  37. togglePin,
  38. markAsRead,
  39. deleteArticle,
  40. refetch
  41. } = useRealArticles(dateFilter, showFollowedOnly, showReadArticles);
  42. const isMobile = useIsMobile();
  43. const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
  44. const [searchQuery, setSearchQuery] = useState('');
  45. const [showFilters, setShowFilters] = useState(true);
  46. const [isAddFeedModalOpen, setIsAddFeedModalOpen] = useState(false);
  47. const [selectedArticle, setSelectedArticle] = useState<NewsItem | null>(null);
  48. const [isArticleModalOpen, setIsArticleModalOpen] = useState(false);
  49. console.log('🏠 Index page - Articles count:', articles.length, 'Loading:', loading, 'User:', !!user);
  50. const filteredNews = useMemo(() => {
  51. let filtered = articles;
  52. if (selectedCategory) {
  53. const category = categories.find(c => c.id === selectedCategory);
  54. if (category) {
  55. filtered = filtered.filter(item => item.category === category.type);
  56. }
  57. }
  58. if (searchQuery) {
  59. filtered = filtered.filter(item => item.title.toLowerCase().includes(searchQuery.toLowerCase()) || item.description.toLowerCase().includes(searchQuery.toLowerCase()) || item.source.toLowerCase().includes(searchQuery.toLowerCase()));
  60. }
  61. return filtered.sort((a, b) => {
  62. if (a.isPinned && !b.isPinned) return -1;
  63. if (!a.isPinned && b.isPinned) return 1;
  64. return new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime();
  65. });
  66. }, [articles, selectedCategory, searchQuery]);
  67. // Separate pinned and regular articles
  68. const pinnedArticles = useMemo(() => {
  69. return filteredNews.filter(article => article.isPinned);
  70. }, [filteredNews]);
  71. const regularArticles = useMemo(() => {
  72. return filteredNews.filter(article => !article.isPinned);
  73. }, [filteredNews]);
  74. const pinnedCount = articles.filter(item => item.isPinned).length;
  75. const unreadCount = articles.filter(item => !item.isRead).length;
  76. // Update document title with unread count
  77. useEffect(() => {
  78. const baseTitle = 'Feeds.Duhaz.fr';
  79. if (unreadCount > 0) {
  80. document.title = `(${unreadCount}) ${baseTitle}`;
  81. } else {
  82. document.title = baseTitle;
  83. }
  84. }, [unreadCount]);
  85. const handleRefresh = () => {
  86. refetch();
  87. toast.success("Flux actualisés");
  88. };
  89. const handleAddFeed = (feedData: any) => {
  90. console.log('Nouveau flux ajouté:', feedData);
  91. toast.success(`Flux "${feedData.name}" ajouté avec succès!`);
  92. };
  93. const handleOpenArticle = (article: NewsItem) => {
  94. setSelectedArticle(article);
  95. setIsArticleModalOpen(true);
  96. };
  97. const handleCloseArticleModal = () => {
  98. setIsArticleModalOpen(false);
  99. setSelectedArticle(null);
  100. };
  101. if (loading) {
  102. return <div className="min-h-screen bg-background flex items-center justify-center">
  103. <div className="flex items-center gap-2">
  104. <Rss className="h-6 w-6 animate-spin text-primary" />
  105. <p>Chargement des articles...</p>
  106. </div>
  107. </div>;
  108. }
  109. return <div className="min-h-screen bg-background">
  110. <Header searchQuery={searchQuery} onSearchChange={setSearchQuery} pinnedCount={pinnedCount} onAddFeedClick={() => setIsAddFeedModalOpen(true)} />
  111. <main className="container mx-auto px-4 py-6">
  112. {/* Message pour les utilisateurs non connectés */}
  113. {!user && (
  114. <div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
  115. <p className="text-blue-800">
  116. Vous consultez les articles en mode visiteur.
  117. <Link to="/auth" className="font-semibold text-blue-600 hover:text-blue-800 ml-1">
  118. Connectez-vous
  119. </Link> pour gérer vos flux et marquer vos articles préférés.
  120. </p>
  121. </div>
  122. )}
  123. {/* Message pour les utilisateurs connectés sans articles suivis */}
  124. {user && articles.length === 0 && (
  125. <div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
  126. <p className="text-amber-800">
  127. Vous ne suivez aucun flux RSS pour le moment.
  128. <Link to="/feeds" className="font-semibold text-amber-600 hover:text-amber-800 ml-1">
  129. Ajoutez des flux
  130. </Link> pour commencer à voir des articles.
  131. </p>
  132. </div>
  133. )}
  134. <div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
  135. {/* Sidebar - Desktop only */}
  136. {!isMobile && (
  137. <div className={`lg:col-span-1 space-y-6 ${!showFilters && 'hidden lg:block'}`}>
  138. <CategoryFilter
  139. categories={categories}
  140. selectedCategory={selectedCategory}
  141. onCategoryChange={setSelectedCategory}
  142. newsCount={articles.length}
  143. pinnedCount={pinnedCount}
  144. articles={articles}
  145. pinnedArticles={pinnedArticles}
  146. dateFilter={dateFilter}
  147. onDateFilterChange={setDateFilter}
  148. showFollowedOnly={showFollowedOnly}
  149. onShowFollowedOnlyChange={handleShowFollowedOnlyChange}
  150. showReadArticles={showReadArticles}
  151. onShowReadArticlesChange={setShowReadArticles}
  152. onTogglePin={togglePin}
  153. onMarkAsRead={markAsRead}
  154. onDeleteArticle={deleteArticle}
  155. onOpenArticle={handleOpenArticle}
  156. />
  157. <div className="bg-card border rounded-lg p-4 space-y-3">
  158. <h3 className="font-semibold text-sm">Statistiques</h3>
  159. <div className="space-y-2 text-sm">
  160. <div className="flex justify-between">
  161. <span className="text-muted-foreground">Articles non lus</span>
  162. <Badge variant="outline">{unreadCount}</Badge>
  163. </div>
  164. <div className="flex justify-between">
  165. <span className="text-muted-foreground">Articles totaux</span>
  166. <Badge variant="outline">{articles.length}</Badge>
  167. </div>
  168. {user && <div className="flex justify-between">
  169. <span className="text-muted-foreground">Épinglés</span>
  170. <Badge variant="secondary">{pinnedCount}</Badge>
  171. </div>}
  172. </div>
  173. </div>
  174. </div>
  175. )}
  176. {/* Main content */}
  177. <div className={`${isMobile ? 'col-span-1' : 'lg:col-span-3'} space-y-6`}>
  178. <div className="flex items-center justify-between">
  179. <div className="flex items-center gap-4">
  180. <h2 className="text-2xl font-bold">
  181. {showReadArticles ? 'Tous les articles' : (user ? 'Articles non lus' : 'Derniers articles')}
  182. </h2>
  183. </div>
  184. <div className="flex items-center gap-2">
  185. {/* Mobile Filter Drawer */}
  186. {isMobile && (
  187. <Drawer>
  188. <DrawerTrigger asChild>
  189. <Button variant="outline" size="sm" className="gap-2">
  190. <Filter className="h-4 w-4" />
  191. Filtres
  192. </Button>
  193. </DrawerTrigger>
  194. <DrawerContent>
  195. <div className="p-4 space-y-6">
  196. <CategoryFilter
  197. categories={categories}
  198. selectedCategory={selectedCategory}
  199. onCategoryChange={setSelectedCategory}
  200. newsCount={articles.length}
  201. pinnedCount={pinnedCount}
  202. articles={articles}
  203. pinnedArticles={pinnedArticles}
  204. dateFilter={dateFilter}
  205. onDateFilterChange={setDateFilter}
  206. showFollowedOnly={showFollowedOnly}
  207. onShowFollowedOnlyChange={handleShowFollowedOnlyChange}
  208. showReadArticles={showReadArticles}
  209. onShowReadArticlesChange={setShowReadArticles}
  210. onTogglePin={togglePin}
  211. onMarkAsRead={markAsRead}
  212. onDeleteArticle={deleteArticle}
  213. onOpenArticle={handleOpenArticle}
  214. />
  215. <div className="bg-card border rounded-lg p-4 space-y-3">
  216. <h3 className="font-semibold text-sm">Statistiques</h3>
  217. <div className="space-y-2 text-sm">
  218. <div className="flex justify-between">
  219. <span className="text-muted-foreground">Articles non lus</span>
  220. <Badge variant="outline">{unreadCount}</Badge>
  221. </div>
  222. <div className="flex justify-between">
  223. <span className="text-muted-foreground">Articles totaux</span>
  224. <Badge variant="outline">{articles.length}</Badge>
  225. </div>
  226. {user && <div className="flex justify-between">
  227. <span className="text-muted-foreground">Épinglés</span>
  228. <Badge variant="secondary">{pinnedCount}</Badge>
  229. </div>}
  230. </div>
  231. </div>
  232. </div>
  233. </DrawerContent>
  234. </Drawer>
  235. )}
  236. {/* Desktop Filter Toggle - keep existing logic */}
  237. {!isMobile && (
  238. <Button variant="outline" size="sm" onClick={() => setShowFilters(!showFilters)} className="lg:hidden gap-2">
  239. <Filter className="h-4 w-4" />
  240. Filtres
  241. </Button>
  242. )}
  243. <Button variant="outline" size="sm" onClick={handleRefresh} className="gap-2">
  244. <RefreshCw className="h-4 w-4" />
  245. Actualiser
  246. </Button>
  247. </div>
  248. </div>
  249. {regularArticles.length === 0 && articles.length > 0 ? <div className="text-center py-12">
  250. <p className="text-muted-foreground text-lg">Aucun article trouvé avec ces filtres</p>
  251. <p className="text-sm text-muted-foreground mt-2">
  252. Essayez de modifier vos filtres ou votre recherche
  253. </p>
  254. {pinnedArticles.length > 0 && (
  255. <p className="text-xs text-muted-foreground mt-2">
  256. ({pinnedArticles.length} article{pinnedArticles.length > 1 ? 's' : ''} épinglé{pinnedArticles.length > 1 ? 's' : ''} dans la sidebar)
  257. </p>
  258. )}
  259. </div> : regularArticles.length === 0 ? <div className="text-center py-12">
  260. <Rss className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
  261. <p className="text-muted-foreground text-lg">Aucun article non lu disponible</p>
  262. <p className="text-sm text-muted-foreground mt-2 mb-4">
  263. {user ? 'Bravo ! Tous vos articles sont lus ou suivez des flux RSS pour voir des articles ici' : 'Aucun article public disponible pour le moment'}
  264. </p>
  265. {pinnedArticles.length > 0 && (
  266. <p className="text-xs text-muted-foreground mb-4">
  267. ({pinnedArticles.length} article{pinnedArticles.length > 1 ? 's' : ''} épinglé{pinnedArticles.length > 1 ? 's' : ''} dans la sidebar)
  268. </p>
  269. )}
  270. {user && <div className="flex gap-2 justify-center">
  271. <Link to="/feeds">
  272. <Button variant="outline">
  273. Gérer les flux
  274. </Button>
  275. </Link>
  276. <Button onClick={() => setIsAddFeedModalOpen(true)}>
  277. <Plus className="h-4 w-4 mr-2" />
  278. Ajouter un flux
  279. </Button>
  280. </div>}
  281. </div> : <div className="space-y-4">
  282. {regularArticles.map(item => <NewsCard key={item.id} news={item} onTogglePin={togglePin} onMarkAsRead={markAsRead} onDelete={deleteArticle} onOpenArticle={handleOpenArticle} />)}
  283. </div>}
  284. </div>
  285. </div>
  286. </main>
  287. {/* Modals */}
  288. {user && <AddFeedModal isOpen={isAddFeedModalOpen} onClose={() => setIsAddFeedModalOpen(false)} onAddFeed={handleAddFeed} categories={categories} />}
  289. <ArticleModal isOpen={isArticleModalOpen} onClose={handleCloseArticleModal} article={selectedArticle} />
  290. </div>;
  291. };
  292. export default Index;