news/frontend/src/routes/articles.tsx

188 lines
6.2 KiB
TypeScript

import { createFileRoute } from '@tanstack/react-router'
import { useInfiniteQuery } from '@tanstack/react-query'
import { fetchArticles } from '@/api/articles'
import { Button } from '@/components/ui/button'
import { addArticleToCollection, createCollection, fetchCollections } from '@/api/collections';
import { useUser } from '@/store/user';
import { useNavigate } from '@tanstack/react-router';
import { Article } from '@/types';
import { useEffect, useRef, useCallback } from 'react';
export const Route = createFileRoute('/articles')({
component: ArticlesPage
})
async function handleAddToCollection(
article: Article,
userId: string | undefined,
navigate: ReturnType<typeof useNavigate>
) {
if (!userId) {
alert('Please login to add articles to collections');
navigate({ to: '/login' });
return;
}
const collectionName = prompt('Enter Collection Name:');
if (!collectionName) return;
try {
// Fetch existing collections
const collections = await fetchCollections();
let collection = collections.find((col) => col.name === collectionName);
// If collection does not exist, create it
if (!collection) {
collection = await createCollection({ name: collectionName, userId });
alert(`Collection '${collectionName}' created successfully!`);
}
// Add article to the collection
await addArticleToCollection(collection.id, {
title: article.title,
content: article.content,
source: article.source,
url: article.url,
publishedAt: new Date(article.publishedAt)
});
alert(`Article added to collection '${collectionName}' successfully!`);
} catch (error: unknown) {
console.error('Failed to add article to collection:', error);
alert('Failed to add article to collection. Please try again.');
}
}
function ArticlesPage() {
const {
data,
isLoading,
isError,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['articles'],
queryFn: async ({ pageParam = undefined }) => {
const response = await fetchArticles(pageParam);
return response;
},
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: undefined as string | undefined,
staleTime: 1000 * 60 // Cache for 1 minute
});
const user = useUser();
const navigate = useNavigate();
const loadMoreRef = useRef<HTMLDivElement>(null);
const handleObserver = useCallback((entries: IntersectionObserverEntry[]) => {
const [target] = entries;
if (target.isIntersecting && hasNextPage && !isFetchingNextPage) {
void fetchNextPage();
}
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
useEffect(() => {
const element = loadMoreRef.current;
const observer = new IntersectionObserver(handleObserver, {
root: null,
rootMargin: '0px',
threshold: 0.1,
});
if (element) {
observer.observe(element);
}
return () => {
if (element) {
observer.unobserve(element);
}
};
}, [handleObserver]);
// Show initial loading state
if (isLoading && !data?.pages?.length) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-4 border-primary border-t-transparent rounded-full"></div>
</div>
);
}
if (isError) {
return (
<div className="p-6 bg-red-50 border border-red-200 rounded-lg">
<h2 className="text-xl font-semibold text-red-800">Error</h2>
<p className="text-red-600">Failed to load articles: {error?.message || 'Unknown error'}</p>
<Button variant="outline" className="mt-4" onClick={() => window.location.reload()}>
Retry
</Button>
</div>
);
}
const allArticles = data?.pages.flatMap(page => page.articles) ?? [];
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">News Articles</h1>
<p className="text-muted-foreground mt-2">Browse the latest news from various sources</p>
</div>
{allArticles.length > 0 ? (
<>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{allArticles.map(article => (
<div
key={article.id}
className="rounded-lg border bg-card shadow-sm overflow-hidden hover:shadow-md transition-shadow"
>
<div className="p-6">
<h2 className="text-xl font-semibold mb-2">{article.title}</h2>
<p className="text-muted-foreground text-sm mb-4 line-clamp-2">
{article.content?.substring(0, 120)}...
</p>
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">{article.source}</span>
<span className="text-xs text-muted-foreground">
{new Date(article.publishedAt).toLocaleDateString()}
</span>
</div>
<Button
variant="outline"
size="sm"
className="mt-4 w-full"
onClick={() => handleAddToCollection(article, user?.id, navigate)}
>
Add to Collection
</Button>
</div>
</div>
))}
</div>
{/* Loading more trigger and indicator */}
<div ref={loadMoreRef} className="flex justify-center py-8">
{isFetchingNextPage ? (
<div className="animate-spin w-6 h-6 border-3 border-primary border-t-transparent rounded-full"></div>
) : hasNextPage ? (
<span className="text-sm text-muted-foreground">Scroll to load more</span>
) : (
<span className="text-sm text-muted-foreground">No more articles to load</span>
)}
</div>
</>
) : (
<div className="text-center p-12 border rounded-lg bg-background">
<h3 className="text-lg font-medium">No articles found</h3>
<p className="text-muted-foreground mt-1">Check back later for new content</p>
</div>
)}
</div>
);
}