import { useState, useEffect, useRef } from 'react'; import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { articlesApi } from '../api/articles'; import { feedsApi } from '../api/feeds'; import { ArticleCard } from '../components/ArticleCard'; import { ReaderView } from '../components/ReaderView'; import { MobileReaderView } from '../components/MobileReaderView'; import { useWebsocket } from '../hooks/useWebsocket'; import type { Article } from '../api/articles'; interface DashboardPageProps { selectedFeedId: string | null; onEnterFocus: () => void; } const ITEMS_PER_PAGE = 24; // Helper Hook for Responsive Split function useIsMobile() { const [isMobile, setIsMobile] = useState(window.matchMedia('(max-width: 768px)').matches); useEffect(() => { const media = window.matchMedia('(max-width: 768px)'); const listener = (e: MediaQueryListEvent) => setIsMobile(e.matches); media.addEventListener('change', listener); return () => media.removeEventListener('change', listener); }, []); return isMobile; } export function DashboardPage({ selectedFeedId, onEnterFocus }: DashboardPageProps) { const queryClient = useQueryClient(); const [selectedArticle, setSelectedArticle] = useState
(null); const [searchQuery, setSearchQuery] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState(''); const isMobile = useIsMobile(); const loadMoreRef = useRef(null); // Debounce search query useEffect(() => { const timer = setTimeout(() => { setDebouncedQuery(searchQuery); }, 500); return () => clearTimeout(timer); }, [searchQuery]); // Real-time sync useWebsocket(); const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ['articles', selectedFeedId, debouncedQuery], queryFn: ({ pageParam = 0 }) => { const options = { limit: ITEMS_PER_PAGE, offset: pageParam, }; if (debouncedQuery) { return articlesApi.search(debouncedQuery, ITEMS_PER_PAGE, pageParam); } if (selectedFeedId === 'favorites') { return articlesApi.list({ ...options, favorite: true }); } return articlesApi.list({ ...options, feed_id: selectedFeedId || undefined }); }, initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { if (lastPage.length < ITEMS_PER_PAGE) return undefined; return allPages.length * ITEMS_PER_PAGE; }, }); const articles = data?.pages.flat() || []; // Intersection Observer for Infinite Scroll useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, { threshold: 0.1 } ); if (loadMoreRef.current) { observer.observe(loadMoreRef.current); } return () => observer.disconnect(); }, [hasNextPage, fetchNextPage, isFetchingNextPage]); const refreshMutation = useMutation({ mutationFn: () => feedsApi.refresh(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['articles'] }); }, }); const toggleReadMutation = useMutation({ mutationFn: ({ id, is_read }: { id: string; is_read: boolean }) => is_read ? articlesApi.markRead(id) : articlesApi.markUnread(id), onMutate: async ({ id, is_read }) => { const queryKey = ['articles', selectedFeedId, debouncedQuery]; await queryClient.cancelQueries({ queryKey }); const previousData = queryClient.getQueryData(queryKey); queryClient.setQueryData(queryKey, (old: any) => { if (!old) return old; return { ...old, pages: old.pages.map((page: Article[]) => page.map((article) => article.id === id ? { ...article, is_read: is_read } : article ) ), }; }); return { previousData }; }, onError: (_err, _newTodo, context) => { const queryKey = ['articles', selectedFeedId, debouncedQuery]; if (context?.previousData) { queryClient.setQueryData(queryKey, context.previousData); } }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['articles'] }); }, }); const toggleFavoriteMutation = useMutation({ mutationFn: articlesApi.toggleFavorite, onSuccess: () => queryClient.invalidateQueries({ queryKey: ['articles'] }), }); const [showScrollTop, setShowScrollTop] = useState(false); const mainRef = useRef(null); // Visibility logic for "Back to Top" button useEffect(() => { const handleScroll = (e: Event) => { const target = e.target as HTMLElement; setShowScrollTop(target.scrollTop > 1000); }; const mainElement = mainRef.current; if (mainElement) { mainElement.addEventListener('scroll', handleScroll); } return () => mainElement?.removeEventListener('scroll', handleScroll); }, []); const scrollToTop = () => { mainRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); }; if (isLoading) { return (

Composition de votre édition...

); } return (
{/* Back to Top Button */}
{/* Header */}

Édition du jour

{debouncedQuery ? `Résultats : "${debouncedQuery}"` : selectedFeedId === 'favorites' ? 'Mes Favoris' : selectedFeedId ? 'Archives du Flux' : 'La Une'}

{new Date().toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })}

Volume IX • No. 42

setSearchQuery(e.target.value)} className="pl-10 pr-4 py-2 bg-white/5 border border-white/10 rounded-full text-[10px] text-paper-white placeholder-paper-muted/40 font-black tracking-widest focus:outline-none focus:ring-1 focus:ring-nature focus:border-nature transition-all w-48 focus:w-72" /> {searchQuery && ( )}
{/* Focus Mode CTA Widget - Compact Version */} {!selectedFeedId && articles && articles.length > 0 && (
{/* decorative circles */}
Nouveauté

Votre session de lecture

{articles.filter(a => !a.is_read).length} articles non lus. Passez en mode Focus.

)} {/* Grid */} {articles.length > 0 ? ( <>
{articles.map((article, index) => (
{ setSelectedArticle(article); if (!article.is_read) { toggleReadMutation.mutate({ id: article.id, is_read: true }); } }} onToggleRead={(id, is_read) => toggleReadMutation.mutate({ id, is_read })} onToggleFavorite={(id) => toggleFavoriteMutation.mutate(id)} />
))}
{/* Infinite Scroll Sentinel */}
{isFetchingNextPage ? ( <>

Chargement supplémentaire...

) : hasNextPage ? (

Défilez pour découvrir la suite

) : (

Fin de votre édition

)}
) : (

{debouncedQuery ? `Aucun article ne correspond à "${debouncedQuery}".` : 'Votre bibliothèque est vide.'}

{debouncedQuery ? "Tentez une recherche plus large." : "Ajoutez un flux pour commencer votre lecture."}

)}
{selectedArticle && ( isMobile ? ( setSelectedArticle(null)} onToggleFavorite={(id) => toggleFavoriteMutation.mutate(id)} onNext={() => { if (!articles) return; const idx = articles.findIndex(a => a.id === selectedArticle.id); if (idx < articles.length - 1) { const nextArticle = articles[idx + 1]; setSelectedArticle(nextArticle); if (!nextArticle.is_read) { toggleReadMutation.mutate({ id: nextArticle.id, is_read: true }); } } else if (hasNextPage) { fetchNextPage(); } else { setSelectedArticle(null); } }} onPrev={() => { if (!articles) return; const idx = articles.findIndex(a => a.id === selectedArticle.id); if (idx > 0) { const prevArticle = articles[idx - 1]; setSelectedArticle(prevArticle); } }} /> ) : ( setSelectedArticle(null)} onToggleFavorite={(id) => toggleFavoriteMutation.mutate(id)} /> ) )}
); }