feat: implement DashboardPage with infinite scrolling, search, and article management features

This commit is contained in:
Antigravity Agent committed 2026-03-31 19:48:13 +02:00
1 parent 908d907629
commit 3a7f7787df
1 file changed
+24 -12
+24 -12
View File
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useMemo } from 'react';
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import Masonry from 'react-masonry-css'; import Masonry from 'react-masonry-css';
import { articlesApi } from '../api/articles'; import { articlesApi } from '../api/articles';
@@ -86,6 +86,18 @@ export function DashboardPage({ selectedFeedId, onEnterFocus }: DashboardPagePro
}); });
const articles = data?.pages.flat() || []; const articles = data?.pages.flat() || [];
const sortedArticles = useMemo(() => {
return [...articles].sort((a, b) => {
// First, sort by read status: unread (false) first
if (a.is_read !== b.is_read) {
return a.is_read ? 1 : -1;
}
// If same read status, sort by date descending
const dateA = a.published_at || a.created_at;
const dateB = b.published_at || b.created_at;
return new Date(dateB) - new Date(dateA);
});
}, [articles]);
// Intersection Observer for Infinite Scroll // Intersection Observer for Infinite Scroll
useEffect(() => { useEffect(() => {
@@ -249,7 +261,7 @@ export function DashboardPage({ selectedFeedId, onEnterFocus }: DashboardPagePro
</header> </header>
{/* Focus Mode CTA Widget - Compact Version */} {/* Focus Mode CTA Widget - Compact Version */}
{!selectedFeedId && articles && articles.length > 0 && ( {!selectedFeedId && sortedArticles && sortedArticles.length > 0 && (
<div className="mb-12 relative overflow-hidden rounded-xl bg-gradient-to-r from-nature to-nature-light shadow-xl items-center flex flex-col md:flex-row p-6 hover:scale-[1.01] transition-transform cursor-pointer group" onClick={onEnterFocus}> <div className="mb-12 relative overflow-hidden rounded-xl bg-gradient-to-r from-nature to-nature-light shadow-xl items-center flex flex-col md:flex-row p-6 hover:scale-[1.01] transition-transform cursor-pointer group" onClick={onEnterFocus}>
{/* decorative circles */} {/* decorative circles */}
<div className="absolute -top-12 -right-12 w-48 h-48 bg-white/10 rounded-full blur-2xl group-hover:bg-white/20 transition-colors pointer-events-none"></div> <div className="absolute -top-12 -right-12 w-48 h-48 bg-white/10 rounded-full blur-2xl group-hover:bg-white/20 transition-colors pointer-events-none"></div>
@@ -265,7 +277,7 @@ export function DashboardPage({ selectedFeedId, onEnterFocus }: DashboardPagePro
Votre session de lecture Votre session de lecture
</h2> </h2>
<p className="text-white/80 font-medium text-xs max-w-lg"> <p className="text-white/80 font-medium text-xs max-w-lg">
{articles.filter(a => !a.is_read).length} articles non lus. Passez en mode Focus. {sortedArticles.filter(a => !a.is_read).length} articles non lus. Passez en mode Focus.
</p> </p>
</div> </div>
@@ -284,14 +296,14 @@ export function DashboardPage({ selectedFeedId, onEnterFocus }: DashboardPagePro
)} )}
{/* Grid */} {/* Grid */}
{articles.length > 0 ? ( {sortedArticles.length > 0 ? (
<> <>
<Masonry <Masonry
breakpointCols={breakpointColumnsObj} breakpointCols={breakpointColumnsObj}
className="flex w-auto -ml-6" className="flex w-auto -ml-6"
columnClassName="pl-6 bg-clip-padding" columnClassName="pl-6 bg-clip-padding"
> >
{articles.map((article, index) => ( {sortedArticles.map((article, index) => (
<div key={`${article.id}-${index}`} className="mb-6"> <div key={`${article.id}-${index}`} className="mb-6">
<ArticleCard <ArticleCard
article={article} article={article}
@@ -345,10 +357,10 @@ export function DashboardPage({ selectedFeedId, onEnterFocus }: DashboardPagePro
onClose={() => setSelectedArticle(null)} onClose={() => setSelectedArticle(null)}
onToggleFavorite={(id) => toggleFavoriteMutation.mutate(id)} onToggleFavorite={(id) => toggleFavoriteMutation.mutate(id)}
onNext={() => { onNext={() => {
if (!articles) return; if (!sortedArticles) return;
const idx = articles.findIndex(a => a.id === selectedArticle.id); const idx = sortedArticles.findIndex(a => a.id === selectedArticle.id);
if (idx < articles.length - 1) { if (idx < sortedArticles.length - 1) {
const nextArticle = articles[idx + 1]; const nextArticle = sortedArticles[idx + 1];
setSelectedArticle(nextArticle); setSelectedArticle(nextArticle);
if (!nextArticle.is_read) { if (!nextArticle.is_read) {
toggleReadMutation.mutate({ id: nextArticle.id, is_read: true }); toggleReadMutation.mutate({ id: nextArticle.id, is_read: true });
@@ -360,10 +372,10 @@ export function DashboardPage({ selectedFeedId, onEnterFocus }: DashboardPagePro
} }
}} }}
onPrev={() => { onPrev={() => {
if (!articles) return; if (!sortedArticles) return;
const idx = articles.findIndex(a => a.id === selectedArticle.id); const idx = sortedArticles.findIndex(a => a.id === selectedArticle.id);
if (idx > 0) { if (idx > 0) {
const prevArticle = articles[idx - 1]; const prevArticle = sortedArticles[idx - 1];
setSelectedArticle(prevArticle); setSelectedArticle(prevArticle);
} }
}} }}