FeedDetail.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import { useState } from 'react';
  2. import { useParams, useNavigate } from 'react-router-dom';
  3. import { useFeedArticles } from '@/hooks/useFeedArticles';
  4. import NewsCard from '@/components/NewsCard';
  5. import ArticleModal from '@/components/ArticleModal';
  6. import { NewsItem } from '@/types/news';
  7. import { Button } from '@/components/ui/button';
  8. import { ArrowLeft, Loader2 } from 'lucide-react';
  9. import {
  10. Pagination,
  11. PaginationContent,
  12. PaginationEllipsis,
  13. PaginationItem,
  14. PaginationLink,
  15. PaginationNext,
  16. PaginationPrevious,
  17. } from '@/components/ui/pagination';
  18. const FeedDetail = () => {
  19. const { feedId } = useParams<{ feedId: string }>();
  20. const navigate = useNavigate();
  21. const [currentPage, setCurrentPage] = useState(1);
  22. const [selectedArticle, setSelectedArticle] = useState<NewsItem | null>(null);
  23. const {
  24. articles,
  25. feedInfo,
  26. loading,
  27. totalCount,
  28. totalPages,
  29. togglePin,
  30. markAsRead,
  31. deleteArticle
  32. } = useFeedArticles(feedId || '', currentPage);
  33. const handleOpenArticle = (article: NewsItem) => {
  34. setSelectedArticle(article);
  35. };
  36. const handleCloseArticleModal = () => {
  37. setSelectedArticle(null);
  38. };
  39. const handlePageChange = (page: number) => {
  40. setCurrentPage(page);
  41. window.scrollTo({ top: 0, behavior: 'smooth' });
  42. };
  43. const renderPaginationItems = () => {
  44. const items = [];
  45. const maxVisible = 5;
  46. if (totalPages <= maxVisible) {
  47. for (let i = 1; i <= totalPages; i++) {
  48. items.push(
  49. <PaginationItem key={i}>
  50. <PaginationLink
  51. onClick={() => handlePageChange(i)}
  52. isActive={currentPage === i}
  53. >
  54. {i}
  55. </PaginationLink>
  56. </PaginationItem>
  57. );
  58. }
  59. } else {
  60. // Always show first page
  61. items.push(
  62. <PaginationItem key={1}>
  63. <PaginationLink
  64. onClick={() => handlePageChange(1)}
  65. isActive={currentPage === 1}
  66. >
  67. 1
  68. </PaginationLink>
  69. </PaginationItem>
  70. );
  71. // Show ellipsis or pages around current
  72. if (currentPage > 3) {
  73. items.push(<PaginationEllipsis key="ellipsis-1" />);
  74. }
  75. const start = Math.max(2, currentPage - 1);
  76. const end = Math.min(totalPages - 1, currentPage + 1);
  77. for (let i = start; i <= end; i++) {
  78. items.push(
  79. <PaginationItem key={i}>
  80. <PaginationLink
  81. onClick={() => handlePageChange(i)}
  82. isActive={currentPage === i}
  83. >
  84. {i}
  85. </PaginationLink>
  86. </PaginationItem>
  87. );
  88. }
  89. if (currentPage < totalPages - 2) {
  90. items.push(<PaginationEllipsis key="ellipsis-2" />);
  91. }
  92. // Always show last page
  93. items.push(
  94. <PaginationItem key={totalPages}>
  95. <PaginationLink
  96. onClick={() => handlePageChange(totalPages)}
  97. isActive={currentPage === totalPages}
  98. >
  99. {totalPages}
  100. </PaginationLink>
  101. </PaginationItem>
  102. );
  103. }
  104. return items;
  105. };
  106. if (loading) {
  107. return (
  108. <div className="min-h-screen flex items-center justify-center">
  109. <Loader2 className="h-8 w-8 animate-spin text-primary" />
  110. </div>
  111. );
  112. }
  113. if (!feedInfo) {
  114. return (
  115. <div className="min-h-screen flex flex-col items-center justify-center gap-4">
  116. <h1 className="text-2xl font-bold">Flux introuvable</h1>
  117. <Button onClick={() => navigate('/')}>
  118. <ArrowLeft className="h-4 w-4 mr-2" />
  119. Retour à l'accueil
  120. </Button>
  121. </div>
  122. );
  123. }
  124. return (
  125. <div className="min-h-screen bg-background">
  126. <div className="container mx-auto px-4 py-8">
  127. {/* Header with back button and feed info */}
  128. <div className="mb-8 space-y-4">
  129. <Button
  130. variant="ghost"
  131. onClick={() => navigate('/')}
  132. className="gap-2"
  133. >
  134. <ArrowLeft className="h-4 w-4" />
  135. Retour
  136. </Button>
  137. <div>
  138. <h1 className="text-3xl font-bold mb-2">{feedInfo.name}</h1>
  139. {feedInfo.description && (
  140. <p className="text-muted-foreground">{feedInfo.description}</p>
  141. )}
  142. <p className="text-sm text-muted-foreground mt-2">
  143. {totalCount} article{totalCount > 1 ? 's' : ''} au total
  144. </p>
  145. </div>
  146. </div>
  147. {/* Articles grid */}
  148. {articles.length === 0 ? (
  149. <div className="text-center py-12">
  150. <p className="text-muted-foreground">Aucun article disponible pour ce flux</p>
  151. </div>
  152. ) : (
  153. <>
  154. <div className="grid gap-6 mb-8">
  155. {articles.map((article) => (
  156. <NewsCard
  157. key={article.id}
  158. news={article}
  159. onTogglePin={togglePin}
  160. onMarkAsRead={markAsRead}
  161. onDelete={deleteArticle}
  162. onOpenArticle={handleOpenArticle}
  163. />
  164. ))}
  165. </div>
  166. {/* Pagination */}
  167. {totalPages > 1 && (
  168. <div className="flex justify-center">
  169. <Pagination>
  170. <PaginationContent>
  171. <PaginationItem>
  172. <PaginationPrevious
  173. onClick={() => currentPage > 1 && handlePageChange(currentPage - 1)}
  174. className={currentPage === 1 ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
  175. />
  176. </PaginationItem>
  177. {renderPaginationItems()}
  178. <PaginationItem>
  179. <PaginationNext
  180. onClick={() => currentPage < totalPages && handlePageChange(currentPage + 1)}
  181. className={currentPage === totalPages ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
  182. />
  183. </PaginationItem>
  184. </PaginationContent>
  185. </Pagination>
  186. </div>
  187. )}
  188. </>
  189. )}
  190. </div>
  191. {/* Article Modal */}
  192. <ArticleModal
  193. article={selectedArticle}
  194. isOpen={!!selectedArticle}
  195. onClose={handleCloseArticleModal}
  196. />
  197. </div>
  198. );
  199. };
  200. export default FeedDetail;