useRealArticles.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  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. export function useRealArticles(dateFilter?: 'today' | 'yesterday' | null, showFollowedOnly?: boolean, showReadArticles?: boolean) {
  7. const [articles, setArticles] = useState<NewsItem[]>([]);
  8. const [loading, setLoading] = useState(true);
  9. const { user } = useAuth();
  10. const fetchArticles = async () => {
  11. try {
  12. setLoading(true);
  13. console.log('🔄 Fetching articles...', { user: !!user, dateFilter, showFollowedOnly });
  14. // Calculate date ranges for filtering
  15. let dateStart = null;
  16. let dateEnd = null;
  17. if (dateFilter === 'today') {
  18. const today = new Date();
  19. today.setHours(0, 0, 0, 0);
  20. dateStart = today.toISOString();
  21. today.setHours(23, 59, 59, 999);
  22. dateEnd = today.toISOString();
  23. } else if (dateFilter === 'yesterday') {
  24. const yesterday = new Date();
  25. yesterday.setDate(yesterday.getDate() - 1);
  26. yesterday.setHours(0, 0, 0, 0);
  27. dateStart = yesterday.toISOString();
  28. yesterday.setHours(23, 59, 59, 999);
  29. dateEnd = yesterday.toISOString();
  30. }
  31. if (user && showFollowedOnly) {
  32. // For authenticated users wanting only followed feeds
  33. const { data: userFeeds, error: userFeedsError } = await supabase
  34. .from('user_feeds')
  35. .select('feed_id')
  36. .eq('user_id', user.id)
  37. .eq('is_followed', true);
  38. if (userFeedsError) {
  39. console.error('❌ Error fetching user feeds:', userFeedsError);
  40. toast.error('Erreur lors du chargement de vos flux');
  41. return;
  42. }
  43. console.log('📋 User followed feeds:', userFeeds);
  44. const followedFeedIds = userFeeds?.map(uf => uf.feed_id) || [];
  45. if (followedFeedIds.length === 0) {
  46. console.log('⚠️ No followed feeds found for user');
  47. setArticles([]);
  48. return;
  49. }
  50. // Fetch pinned articles first (without date filter)
  51. const pinnedQuery = supabase
  52. .from('articles')
  53. .select(`
  54. *,
  55. feeds!inner(name, category),
  56. user_articles!inner(is_read, is_pinned)
  57. `)
  58. .in('feed_id', followedFeedIds)
  59. .eq('user_articles.user_id', user.id)
  60. .eq('user_articles.is_pinned', true)
  61. .eq('user_articles.is_read', false);
  62. const { data: pinnedArticles, error: pinnedError } = await pinnedQuery
  63. .order('published_at', { ascending: false })
  64. .limit(50);
  65. if (pinnedError) {
  66. console.error('❌ Error fetching pinned articles:', pinnedError);
  67. }
  68. // Fetch regular articles (with date filter if specified)
  69. let regularQuery;
  70. if (!showReadArticles) {
  71. // Optimize: exclude read articles at SQL level
  72. regularQuery = supabase
  73. .from('articles')
  74. .select(`
  75. *,
  76. feeds!inner(name, category),
  77. user_articles!left(is_read, is_pinned)
  78. `)
  79. .in('feed_id', followedFeedIds)
  80. .or(`user_articles.is.null,user_articles.is_read.eq.false`);
  81. } else {
  82. // Include all articles (read and unread)
  83. regularQuery = supabase
  84. .from('articles')
  85. .select(`
  86. *,
  87. feeds!inner(name, category),
  88. user_articles(is_read, is_pinned)
  89. `)
  90. .in('feed_id', followedFeedIds);
  91. }
  92. // Apply date filter to regular articles only
  93. if (dateStart && dateEnd) {
  94. regularQuery = regularQuery.gte('published_at', dateStart).lte('published_at', dateEnd);
  95. }
  96. const { data: regularArticles, error: regularError } = await regularQuery
  97. .order('published_at', { ascending: false })
  98. .limit(100);
  99. if (regularError) {
  100. console.error('❌ Error fetching regular articles:', regularError);
  101. toast.error('Erreur lors du chargement des articles');
  102. return;
  103. }
  104. // Combine articles and remove duplicates
  105. const allArticles = [...(pinnedArticles || []), ...(regularArticles || [])];
  106. const uniqueArticles = allArticles.filter((article, index, self) =>
  107. index === self.findIndex(a => a.id === article.id)
  108. );
  109. console.log('📰 Articles found (SQL filtered for read articles):', {
  110. pinned: pinnedArticles?.length || 0,
  111. regular: regularArticles?.length || 0,
  112. unique: uniqueArticles.length,
  113. showReadArticles,
  114. sqlFiltered: !showReadArticles
  115. });
  116. // Transform to NewsItem format (read articles already filtered at SQL level when showReadArticles=false)
  117. const transformedArticles: NewsItem[] = uniqueArticles
  118. ?.map(article => ({
  119. id: article.id,
  120. title: article.title,
  121. description: article.description || '',
  122. content: article.content || '',
  123. source: article.feeds.name,
  124. category: article.feeds.category as NewsItem['category'],
  125. publishedAt: article.published_at,
  126. readTime: article.read_time || 5,
  127. isPinned: article.user_articles[0]?.is_pinned || false,
  128. isRead: article.user_articles[0]?.is_read || false,
  129. url: article.url || undefined,
  130. imageUrl: article.image_url || undefined,
  131. feedId: article.feed_id
  132. })) || [];
  133. setArticles(transformedArticles);
  134. } else {
  135. // For users wanting all articles or visitors - show all articles from all feeds
  136. console.log('👤 Loading all articles (visitor or showFollowedOnly=false)');
  137. let pinnedArticles = [];
  138. let regularArticles = [];
  139. if (user) {
  140. // Fetch pinned articles first (without date filter) for authenticated users
  141. const pinnedQuery = supabase
  142. .from('articles')
  143. .select(`
  144. *,
  145. feeds!inner(name, category),
  146. user_articles!inner(is_read, is_pinned)
  147. `)
  148. .eq('user_articles.user_id', user.id)
  149. .eq('user_articles.is_pinned', true);
  150. const { data: pinnedData, error: pinnedError } = await pinnedQuery
  151. .order('published_at', { ascending: false })
  152. .limit(50);
  153. if (pinnedError) {
  154. console.error('❌ Error fetching pinned articles:', pinnedError);
  155. } else {
  156. pinnedArticles = pinnedData || [];
  157. }
  158. }
  159. // Fetch regular articles (NO date filter for "All articles" mode)
  160. let regularQuery = supabase
  161. .from('articles')
  162. .select(`
  163. *,
  164. feeds!inner(name, category),
  165. user_articles(is_read, is_pinned)
  166. `);
  167. // Don't apply date filter in "All articles" mode - show everything
  168. const { data: regularData, error: regularError } = await regularQuery
  169. .order('published_at', { ascending: false })
  170. .limit(100);
  171. if (regularError) {
  172. console.error('❌ Error fetching regular articles:', regularError);
  173. toast.error('Erreur lors du chargement des articles');
  174. return;
  175. }
  176. regularArticles = regularData || [];
  177. // Combine articles and remove duplicates
  178. const allArticles = [...pinnedArticles, ...regularArticles];
  179. const uniqueArticles = allArticles.filter((article, index, self) =>
  180. index === self.findIndex(a => a.id === article.id)
  181. );
  182. console.log('📰 All articles found:', {
  183. pinned: pinnedArticles.length,
  184. regular: regularArticles.length,
  185. unique: uniqueArticles.length
  186. });
  187. console.log('🔍 Before filtering - Articles details:', {
  188. total: uniqueArticles.length,
  189. withFeeds: uniqueArticles.filter(a => a.feeds).length,
  190. withUserArticles: uniqueArticles.filter(a => a.user_articles && a.user_articles.length > 0).length,
  191. readArticles: uniqueArticles.filter(a => a.user_articles?.[0]?.is_read).length,
  192. showReadArticles,
  193. sampleArticle: uniqueArticles[0] ? {
  194. id: uniqueArticles[0].id,
  195. title: uniqueArticles[0].title.substring(0, 50),
  196. feeds: !!uniqueArticles[0].feeds,
  197. userArticles: uniqueArticles[0].user_articles?.length || 0,
  198. isRead: uniqueArticles[0].user_articles?.[0]?.is_read
  199. } : null
  200. });
  201. // Transform to NewsItem format and conditionally filter read articles
  202. const transformedArticles: NewsItem[] = uniqueArticles
  203. ?.filter(article => {
  204. const hasFeeds = !!article.feeds;
  205. const userArticle = article.user_articles?.[0];
  206. const isRead = userArticle?.is_read || false;
  207. const shouldShow = showReadArticles || !isRead;
  208. if (!hasFeeds) {
  209. console.log('❌ Article filtered out - no feeds:', article.id);
  210. }
  211. if (!shouldShow) {
  212. console.log('❌ Article filtered out - read filter:', article.id, { isRead, showReadArticles });
  213. }
  214. return hasFeeds && shouldShow;
  215. })
  216. ?.map(article => ({
  217. id: article.id,
  218. title: article.title,
  219. description: article.description || '',
  220. content: article.content || '',
  221. source: article.feeds.name,
  222. category: article.feeds.category as NewsItem['category'],
  223. publishedAt: article.published_at,
  224. readTime: article.read_time || 5,
  225. isPinned: user ? (article.user_articles[0]?.is_pinned || false) : false,
  226. isRead: user ? (article.user_articles[0]?.is_read || false) : false,
  227. url: article.url || undefined,
  228. imageUrl: article.image_url || undefined,
  229. feedId: article.feed_id
  230. })) || [];
  231. console.log('✅ After filtering - Final articles:', {
  232. transformedCount: transformedArticles.length,
  233. sampleTransformed: transformedArticles[0] ? {
  234. id: transformedArticles[0].id,
  235. title: transformedArticles[0].title.substring(0, 50),
  236. isRead: transformedArticles[0].isRead,
  237. isPinned: transformedArticles[0].isPinned
  238. } : null
  239. });
  240. setArticles(transformedArticles);
  241. }
  242. } catch (error) {
  243. console.error('💥 Error in fetchArticles:', error);
  244. toast.error('Erreur lors du chargement des articles');
  245. } finally {
  246. setLoading(false);
  247. }
  248. };
  249. const togglePin = async (articleId: string) => {
  250. if (!user) {
  251. toast.error('Vous devez être connecté pour épingler un article');
  252. return;
  253. }
  254. try {
  255. const article = articles.find(a => a.id === articleId);
  256. if (!article) return;
  257. const { error } = await supabase
  258. .from('user_articles')
  259. .upsert({
  260. user_id: user.id,
  261. article_id: articleId,
  262. is_pinned: !article.isPinned,
  263. is_read: article.isRead
  264. }, {
  265. onConflict: 'user_id,article_id'
  266. });
  267. if (error) {
  268. toast.error('Erreur lors de la mise à jour');
  269. return;
  270. }
  271. setArticles(prev => prev.map(item =>
  272. item.id === articleId ? { ...item, isPinned: !item.isPinned } : item
  273. ));
  274. toast.success(article.isPinned ? "Article retiré des épinglés" : "Article épinglé");
  275. } catch (error) {
  276. console.error('Error toggling pin:', error);
  277. toast.error('Erreur lors de la mise à jour');
  278. }
  279. };
  280. const markAsRead = async (articleId: string) => {
  281. if (!user) return;
  282. try {
  283. const article = articles.find(a => a.id === articleId);
  284. if (!article || article.isRead) return;
  285. const { error } = await supabase
  286. .from('user_articles')
  287. .upsert({
  288. user_id: user.id,
  289. article_id: articleId,
  290. is_read: true,
  291. is_pinned: article.isPinned,
  292. read_at: new Date().toISOString()
  293. }, {
  294. onConflict: 'user_id,article_id'
  295. });
  296. if (error) {
  297. console.error('Error marking as read:', error);
  298. return;
  299. }
  300. // Update local state: remove if not showing read articles, otherwise mark as read
  301. if (!showReadArticles) {
  302. setArticles(prev => prev.filter(item => item.id !== articleId));
  303. toast.success("Article marqué comme lu");
  304. } else {
  305. setArticles(prev => prev.map(item => item.id === articleId ? { ...item, isRead: true } : item));
  306. toast.success("Article marqué comme lu");
  307. }
  308. } catch (error) {
  309. console.error('Error marking as read:', error);
  310. }
  311. };
  312. const deleteArticle = async (articleId: string) => {
  313. if (!user) {
  314. toast.error('Vous devez être connecté pour supprimer un article');
  315. return;
  316. }
  317. try {
  318. // Remove from user's view by deleting user_articles record
  319. const { error } = await supabase
  320. .from('user_articles')
  321. .delete()
  322. .eq('user_id', user.id)
  323. .eq('article_id', articleId);
  324. if (error) {
  325. toast.error('Erreur lors de la suppression');
  326. return;
  327. }
  328. setArticles(prev => prev.filter(item => item.id !== articleId));
  329. toast.success("Article supprimé de votre vue");
  330. } catch (error) {
  331. console.error('Error deleting article:', error);
  332. toast.error('Erreur lors de la suppression');
  333. }
  334. };
  335. const fetchRSSContent = async (feedId: string, feedUrl: string) => {
  336. try {
  337. toast.info('Récupération du contenu RSS...');
  338. const { data, error } = await supabase.functions.invoke('fetch-rss', {
  339. body: { feedId, feedUrl }
  340. });
  341. if (error) {
  342. throw error;
  343. }
  344. if (data.success) {
  345. toast.success(`${data.articlesProcessed} articles récupérés`);
  346. await fetchArticles(); // Refresh articles
  347. } else {
  348. throw new Error(data.error || 'Erreur lors de la récupération RSS');
  349. }
  350. } catch (error) {
  351. console.error('Error fetching RSS:', error);
  352. toast.error('Erreur lors de la récupération du contenu RSS');
  353. }
  354. };
  355. useEffect(() => {
  356. fetchArticles();
  357. }, [user, dateFilter, showFollowedOnly, showReadArticles]);
  358. return {
  359. articles,
  360. loading,
  361. togglePin,
  362. markAsRead,
  363. deleteArticle,
  364. refetch: fetchArticles,
  365. fetchRSSContent
  366. };
  367. }