This commit is contained in:
Tomas Dvorak
2025-10-21 15:02:05 +02:00
parent 68e69e00cc
commit 63700eedb2
103 changed files with 12442 additions and 446 deletions
+85 -15
View File
@@ -1,5 +1,5 @@
import React, { useRef, useState, useCallback } from 'react';
import { Box, Image, Heading, Text, VStack, HStack, Skeleton, Button, IconButton, Flex, useBreakpointValue, Container } from '@chakra-ui/react';
import React, { useState, useCallback, useMemo, useEffect } from 'react';
import { Box, Image, Heading, Text, VStack, HStack, Skeleton, Button, IconButton, Flex, Container } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { getArticles, getFeaturedArticles, Article } from '../../services/articles';
import { Link as RouterLink } from 'react-router-dom';
@@ -12,6 +12,18 @@ import { wrap } from 'popmotion';
const MotionBox = motion(Box);
const MotionImage = motion(Image);
type FallbackArticle = Partial<Article> & {
id?: number | string;
title: string;
excerpt?: string;
image?: string;
date?: string;
};
interface BlogSwiperProps {
fallbackArticles?: FallbackArticle[];
}
const variants = {
enter: (direction: number) => ({
x: direction > 0 ? 1000 : -1000,
@@ -150,8 +162,7 @@ const HeroSlide: React.FC<{ article: Article }> = ({ article }) => {
);
};
const BlogSwiper: React.FC = () => {
const [page, setPage] = useState(0);
const BlogSwiper: React.FC<BlogSwiperProps> = ({ fallbackArticles = [] }) => {
const [[slideIndex, direction], setSlideIndex] = useState([0, 0]);
const { data: featuredData, isLoading: loadingFeatured } = useQuery({
queryKey: ['featured-articles', { page: 1, page_size: 5 }],
@@ -164,8 +175,32 @@ const BlogSwiper: React.FC = () => {
enabled: Boolean(!loadingFeatured && !(featuredData?.data?.length)),
});
const articles = (featuredData?.data?.length ? featuredData.data : (latestData?.data || []));
const articleIndex = wrap(0, articles.length, slideIndex);
const normalizedFallback = useMemo<Article[]>(() => fallbackArticles.map((item, index) => ({
id: typeof item.id === 'number' ? item.id : index,
title: item.title,
content: item.content ?? item.excerpt ?? '',
image_url: item.image_url ?? item.image ?? undefined,
author: item.author,
category: typeof item.category === 'string' ? { id: index, name: item.category } : item.category,
category_name: typeof item.category === 'string' ? item.category : item.category_name,
slug: item.slug,
created_at: item.created_at ?? item.published_at ?? item.date ?? new Date().toISOString(),
published: item.published ?? true,
})), [fallbackArticles]);
const remoteArticles = useMemo<Article[]>(() => {
if (featuredData?.data?.length) {
return featuredData.data;
}
if (latestData?.data?.length) {
return latestData.data;
}
return [];
}, [featuredData?.data, latestData?.data]);
const articles = remoteArticles.length ? remoteArticles : normalizedFallback;
const articleCount = articles.length;
const articleIndex = articleCount > 0 ? wrap(0, articleCount, slideIndex) : 0;
const paginate = useCallback(
(newDirection: number) => {
setSlideIndex([slideIndex + newDirection, newDirection]);
@@ -174,17 +209,27 @@ const BlogSwiper: React.FC = () => {
);
// Auto-advance slides
React.useEffect(() => {
if (articles.length <= 1) return;
useEffect(() => {
if (articleCount <= 1) return;
const timer = setInterval(() => {
paginate(1);
}, 8000);
return () => clearInterval(timer);
}, [articles.length, paginate]);
if (loadingFeatured) {
return () => clearInterval(timer);
}, [articleCount, paginate]);
useEffect(() => {
if (articleCount === 0 && slideIndex !== 0) {
setSlideIndex([0, 0]);
} else if (articleIndex >= articleCount && articleCount > 0) {
setSlideIndex([0, 0]);
}
}, [articleCount, articleIndex, slideIndex]);
const isLoading = loadingFeatured && !remoteArticles.length && !normalizedFallback.length;
if (isLoading) {
return (
<Skeleton
w="100%"
@@ -194,10 +239,35 @@ const BlogSwiper: React.FC = () => {
);
}
if (!articles.length) return null;
if (!articleCount) {
return (
<Box
position="relative"
w="100%"
h={{ base: '480px', md: '560px' }}
borderRadius={{ base: 'none', md: 'xl' }}
bgGradient="linear(to-br, blackAlpha.600, blackAlpha.800)"
display="flex"
alignItems="center"
justifyContent="center"
color="whiteAlpha.800"
textAlign="center"
px={8}
>
<VStack spacing={4}>
<Heading size="lg">Žádné články k zobrazení</Heading>
<Text maxW="lg">
Přidejte prosím nové články nebo nastavte vybrané příspěvky, aby se karusel mohl zobrazit.
</Text>
</VStack>
</Box>
);
}
const currentArticle = articles[articleIndex];
if (!currentArticle) return null;
if (!currentArticle) {
return null;
}
return (
<Box position="relative" w="100%" overflow="hidden">