import React from 'react'; import { Box, Container, Heading, VStack, Image, Text, Skeleton, LinkBox, HStack, Select, Badge, useColorModeValue } from '@chakra-ui/react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { getArticles, Article, Paginated } from '../services/articles'; import { Link as RouterLink } from 'react-router-dom'; import { assetUrl } from '../utils/url'; import MainLayout from '../components/layout/MainLayout'; import { getCategories, CategoryItem } from '../services/categories'; import SponsorsSection from '../components/common/SponsorsSection'; import NewsletterCTA from '../components/common/NewsletterCTA'; import { Eye, Clock } from 'lucide-react'; const BlogTile: React.FC<{ article: Article }> = ({ article }) => { const link = article.slug ? `/news/${article.slug}` : `/articles/${article.id}`; const readTime = article.read_time || article.estimated_read_minutes; const viewCount = article.view_count; const bgColor = useColorModeValue('white', 'gray.800'); return ( {article.title} {/* Stats badges at top */} {(readTime || (viewCount && viewCount > 0)) && ( {readTime && ( {readTime} min )} {viewCount && viewCount > 0 && ( {viewCount} )} )} {article.title} ); }; const BlogPage: React.FC = () => { const pageSize = 18; const [categories, setCategories] = React.useState([]); const [categoryId, setCategoryId] = React.useState(''); const borderColor = useColorModeValue('gray.200', 'gray.700'); const textColor = useColorModeValue('gray.500', 'gray.400'); React.useEffect(() => { (async () => { try { const list = await getCategories(); setCategories(list || []); } catch {} })(); }, []); const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage, } = useInfiniteQuery>( ['articles-public', { page_size: pageSize, published: true, category_id: categoryId || undefined }], ({ pageParam = 1 }) => getArticles({ page: pageParam, page_size: pageSize, published: true, ...(categoryId ? { category_id: Number(categoryId) } : {}), }), { getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce((sum, p) => sum + (p?.data?.length || 0), 0); if (!lastPage) return undefined; if (loaded < (lastPage.total || 0)) return allPages.length + 1; return undefined; }, } ); const articles = data?.pages?.flatMap((p) => p?.data || []) || []; // Infinite scroll via intersection observer const sentinelRef = React.useRef(null); React.useEffect(() => { if (!hasNextPage || !sentinelRef.current) return; const el = sentinelRef.current; const io = new IntersectionObserver((entries) => { const first = entries[0]; if (first.isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, { rootMargin: '400px' }); io.observe(el); return () => io.disconnect(); }, [hasNextPage, isFetchingNextPage, fetchNextPage]); return ( {/* Header like blog.html */} Blog {!!categories.length && ( )} {/* Masonry using CSS columns */} {isLoading && Array.from({ length: 9 }).map((_, i) => ( ))} {!isLoading && articles.map((a) => ( ))} {!isLoading && !articles.length && ( Žádné články k zobrazení. )} {/* Infinite scroll sentinel */} {isFetchingNextPage && ( Načítání… )} {/* Newsletter CTA */} {/* Sponsors Section */} ); }; export default BlogPage;