useRealArticles.tsx 14 KB

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