mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
dev day #67
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user