useFeedArticles.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import { useState, useEffect } from 'react';
  2. import { supabase } from '@/integrations/supabase/client';
  3. import { useAuth } from './useAuth';
  4. import { NewsItem } from '@/types/news';
  5. import { toast } from 'sonner';
  6. const ARTICLES_PER_PAGE = 100;
  7. const FETCH_LIMIT = 200;
  8. interface FeedInfo {
  9. name: string;
  10. description: string | null;
  11. category: string;
  12. type: string;
  13. }
  14. export function useFeedArticles(feedId: string, page: number = 1) {
  15. const [articles, setArticles] = useState<NewsItem[]>([]);
  16. const [feedInfo, setFeedInfo] = useState<FeedInfo | null>(null);
  17. const [loading, setLoading] = useState(true);
  18. const [totalCount, setTotalCount] = useState(0);
  19. const { user } = useAuth();
  20. const fetchFeedArticles = async () => {
  21. try {
  22. setLoading(true);
  23. console.log('🔄 Fetching articles for feed:', feedId, 'page:', page);
  24. // Fetch feed info
  25. const { data: feed, error: feedError } = await supabase
  26. .from('feeds')
  27. .select('name, description, category, type')
  28. .eq('id', feedId)
  29. .single();
  30. if (feedError) {
  31. console.error('❌ Error fetching feed info:', feedError);
  32. toast.error('Flux introuvable');
  33. return;
  34. }
  35. setFeedInfo(feed);
  36. // Get total count for pagination
  37. const { count } = await supabase
  38. .from('articles')
  39. .select('*', { count: 'exact', head: true })
  40. .eq('feed_id', feedId);
  41. setTotalCount(count || 0);
  42. // Calculate pagination
  43. const from = (page - 1) * ARTICLES_PER_PAGE;
  44. const to = from + ARTICLES_PER_PAGE - 1;
  45. // Fetch articles with pagination - fetch more than displayed
  46. const fetchFrom = (page - 1) * FETCH_LIMIT;
  47. const fetchTo = fetchFrom + FETCH_LIMIT - 1;
  48. let query = supabase
  49. .from('articles')
  50. .select(`
  51. *,
  52. feeds!inner(name, category),
  53. user_articles(is_read, is_pinned)
  54. `)
  55. .eq('feed_id', feedId)
  56. .order('published_at', { ascending: false })
  57. .range(fetchFrom, fetchTo);
  58. const { data: articlesData, error: articlesError } = await query;
  59. if (articlesError) {
  60. console.error('❌ Error fetching articles:', articlesError);
  61. toast.error('Erreur lors du chargement des articles');
  62. return;
  63. }
  64. console.log('📰 Articles found:', articlesData?.length || 0);
  65. // Transform to NewsItem format
  66. const transformedArticles: NewsItem[] = articlesData
  67. ?.map(article => ({
  68. id: article.id,
  69. title: article.title,
  70. description: article.description || '',
  71. content: article.content || '',
  72. source: article.feeds.name,
  73. category: article.feeds.category as NewsItem['category'],
  74. publishedAt: article.published_at,
  75. readTime: article.read_time || 5,
  76. isPinned: user ? (article.user_articles[0]?.is_pinned || false) : false,
  77. isRead: user ? (article.user_articles[0]?.is_read || false) : false,
  78. url: article.url || undefined,
  79. imageUrl: article.image_url || undefined,
  80. feedId: article.feed_id,
  81. lastSeenAt: article.last_seen_at || undefined
  82. })) || [];
  83. setArticles(transformedArticles.slice(0, ARTICLES_PER_PAGE));
  84. } catch (error) {
  85. console.error('💥 Error in fetchFeedArticles:', error);
  86. toast.error('Erreur lors du chargement des articles');
  87. } finally {
  88. setLoading(false);
  89. }
  90. };
  91. const togglePin = async (articleId: string) => {
  92. if (!user) {
  93. toast.error('Vous devez être connecté pour épingler un article');
  94. return;
  95. }
  96. try {
  97. const article = articles.find(a => a.id === articleId);
  98. if (!article) return;
  99. const { error } = await supabase
  100. .from('user_articles')
  101. .upsert({
  102. user_id: user.id,
  103. article_id: articleId,
  104. is_pinned: !article.isPinned,
  105. is_read: article.isRead
  106. }, {
  107. onConflict: 'user_id,article_id'
  108. });
  109. if (error) {
  110. toast.error('Erreur lors de la mise à jour');
  111. return;
  112. }
  113. setArticles(prev => prev.map(item =>
  114. item.id === articleId ? { ...item, isPinned: !item.isPinned } : item
  115. ));
  116. toast.success(article.isPinned ? "Article retiré des épinglés" : "Article épinglé");
  117. } catch (error) {
  118. console.error('Error toggling pin:', error);
  119. toast.error('Erreur lors de la mise à jour');
  120. }
  121. };
  122. const markAsRead = async (articleId: string) => {
  123. if (!user) return;
  124. try {
  125. const article = articles.find(a => a.id === articleId);
  126. if (!article || article.isRead) return;
  127. const { error } = await supabase
  128. .from('user_articles')
  129. .upsert({
  130. user_id: user.id,
  131. article_id: articleId,
  132. is_read: true,
  133. is_pinned: article.isPinned,
  134. read_at: new Date().toISOString()
  135. }, {
  136. onConflict: 'user_id,article_id'
  137. });
  138. if (error) {
  139. console.error('Error marking as read:', error);
  140. return;
  141. }
  142. setArticles(prev => prev.map(item =>
  143. item.id === articleId ? { ...item, isRead: true } : item
  144. ));
  145. } catch (error) {
  146. console.error('Error marking as read:', error);
  147. }
  148. };
  149. const deleteArticle = async (articleId: string) => {
  150. if (!user) {
  151. toast.error('Vous devez être connecté pour supprimer un article');
  152. return;
  153. }
  154. try {
  155. const { error } = await supabase
  156. .from('user_articles')
  157. .delete()
  158. .eq('user_id', user.id)
  159. .eq('article_id', articleId);
  160. if (error) {
  161. toast.error('Erreur lors de la suppression');
  162. return;
  163. }
  164. setArticles(prev => prev.filter(item => item.id !== articleId));
  165. toast.success("Article supprimé de votre vue");
  166. } catch (error) {
  167. console.error('Error deleting article:', error);
  168. toast.error('Erreur lors de la suppression');
  169. }
  170. };
  171. useEffect(() => {
  172. if (feedId) {
  173. fetchFeedArticles();
  174. }
  175. }, [feedId, page, user]);
  176. const totalPages = Math.ceil(totalCount / ARTICLES_PER_PAGE);
  177. return {
  178. articles,
  179. feedInfo,
  180. loading,
  181. totalCount,
  182. totalPages,
  183. currentPage: page,
  184. togglePin,
  185. markAsRead,
  186. deleteArticle,
  187. refetch: fetchFeedArticles
  188. };
  189. }