useFeedArticles.tsx 6.1 KB

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