From e270495a17959228764a347c75a53f2c40915967 Mon Sep 17 00:00:00 2001 From: Samandar Turgunboyev Date: Tue, 10 Feb 2026 20:41:53 +0500 Subject: [PATCH] update carousel --- .../categories/ui/CategoryAccordion.tsx | 358 ++++++++++++++++++ .../categories/ui/category-carousel.tsx | 106 +++--- src/widgets/welcome/ui/index.tsx | 10 +- 3 files changed, 404 insertions(+), 70 deletions(-) create mode 100644 src/widgets/categories/ui/CategoryAccordion.tsx diff --git a/src/widgets/categories/ui/CategoryAccordion.tsx b/src/widgets/categories/ui/CategoryAccordion.tsx new file mode 100644 index 0000000..94b21d0 --- /dev/null +++ b/src/widgets/categories/ui/CategoryAccordion.tsx @@ -0,0 +1,358 @@ +'use client'; + +import { CategoryResult } from '@/shared/config/api/category/type'; +import { product_api } from '@/shared/config/api/product/api'; +import { useRouter } from '@/shared/config/i18n/navigation'; +import { cn } from '@/shared/lib/utils'; +import { Button } from '@/shared/ui/button'; +import { + Carousel, + CarouselContent, + CarouselItem, + type CarouselApi, +} from '@/shared/ui/carousel'; +import { ProductCard } from '@/widgets/categories/ui/product-card'; +import { useQuery } from '@tanstack/react-query'; +import { ChevronDown, ChevronLeft, ChevronRight, Package } from 'lucide-react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; + +interface CategoryAccordionProps { + category: CategoryResult; +} + +const CategoryAccordion = memo(function CategoryAccordion({ + category, +}: CategoryAccordionProps) { + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const [carousels, setCarousels] = useState>( + new Map(), + ); + const [scrollStates, setScrollStates] = useState< + Map + >(new Map()); + + // Ref to track if event listeners are already attached + const listenersAttached = useRef>(new Set()); + + const toggle = useCallback(() => setIsOpen((prev) => !prev), []); + + // Separate effect to handle carousel listeners + useEffect(() => { + if (carousels.size === 0) return; + + const updateScrollState = (subId: string, api: CarouselApi) => { + if (!api) return; // Guard clause + + setScrollStates((prev) => { + const newMap = new Map(prev); + newMap.set(subId, { + prev: api.canScrollPrev(), + next: api.canScrollNext(), + }); + return newMap; + }); + }; + + const cleanupFunctions: (() => void)[] = []; + + carousels.forEach((api, subId) => { + // Skip if api is undefined or listeners already attached + if (!api || listenersAttached.current.has(subId)) return; + + const handleUpdate = () => updateScrollState(subId, api); + + // Initial update + handleUpdate(); + + // Attach listeners + api.on('select', handleUpdate); + api.on('reInit', handleUpdate); + + // Mark as attached + listenersAttached.current.add(subId); + + // Cleanup function + cleanupFunctions.push(() => { + api.off('select', handleUpdate); + api.off('reInit', handleUpdate); + listenersAttached.current.delete(subId); + }); + }); + + return () => { + cleanupFunctions.forEach((cleanup) => cleanup()); + }; + }, [carousels]); + + const handleSetApi = useCallback( + (subId: string, api: CarouselApi | undefined) => { + if (!api) return; + + setCarousels((prev) => { + // Only update if this is a new or different API + const existing = prev.get(subId); + if (existing === api) return prev; + + const newMap = new Map(prev); + newMap.set(subId, api); + return newMap; + }); + }, + [], + ); + + const scrollPrev = useCallback( + (subId: string) => { + const api = carousels.get(subId); + if (api) { + api.scrollPrev(); + } + }, + [carousels], + ); + + const scrollNext = useCallback( + (subId: string) => { + const api = carousels.get(subId); + if (api) { + api.scrollNext(); + } + }, + [carousels], + ); + + const { data: productsData, isLoading } = useQuery({ + queryKey: ['category_products', category.id], + queryFn: async () => { + const results = await Promise.all( + category.product_types.map((sub) => + product_api.list({ page: 1, page_size: 16, product_type_id: sub.id }), + ), + ); + return results.map((r, i) => ({ + subCategory: category.product_types[i], + products: r.data.results.filter((p) => p.state === 'A'), + })); + }, + enabled: isOpen, + staleTime: 5 * 60 * 1000, + }); + + return ( +
+ {/* Header */} +
+
+
+
+ +
+
+

+ {category.name} +

+
+ + + {category.product_types.length} + + + kategoriya + + +
+
+
+ +
+ +
+
+ +
+
+ + {/* Content */} +
+ {isLoading ? ( +
+ {Array.from({ length: 2 }).map((_, idx) => ( +
+
+
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+
+ ))} +
+ ) : ( +
+ {productsData?.map((sub) => { + if (!sub.products.length) return null; + + const subId = sub.subCategory.id.toString(); + const scrollState = scrollStates.get(subId) || { + prev: false, + next: false, + }; + + return ( +
+ {/* Subcategory Header */} +
+
+ router.push( + `/category/${category.id}/${sub.subCategory.id}/`, + ) + } + > +
+

+ {sub.subCategory.name} +

+
+ +
+
+ + + {sub.products.length} + + mahsulot + +
+ + {/* Carousel */} +
+ handleSetApi(subId, api)} + > + + {sub.products.map((product) => ( + + + + ))} + + + + {/* Navigation Buttons */} + + +
+
+ ); + })} +
+ )} +
+
+ ); +}); + +export default CategoryAccordion; diff --git a/src/widgets/categories/ui/category-carousel.tsx b/src/widgets/categories/ui/category-carousel.tsx index db6531a..09ea963 100644 --- a/src/widgets/categories/ui/category-carousel.tsx +++ b/src/widgets/categories/ui/category-carousel.tsx @@ -7,16 +7,25 @@ import { cn } from '@/shared/lib/utils'; import { Button } from '@/shared/ui/button'; import { Carousel, - CarouselApi, CarouselContent, CarouselItem, + type CarouselApi, } from '@/shared/ui/carousel'; +import { ProductCard } from '@/widgets/categories/ui/product-card'; import { useQuery } from '@tanstack/react-query'; import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; -import { ProductCard } from './product-card'; +import { memo, useEffect, useRef, useState } from 'react'; -export function CategoryCarousel({ category }: { category: ProductTypes }) { +////////////////////////// +/// CategoryCarousel optimized +////////////////////////// +interface CategoryCarouselProps { + category: ProductTypes; +} + +const CategoryCarousel = memo(function CategoryCarousel({ + category, +}: CategoryCarouselProps) { const router = useRouter(); const [api, setApi] = useState(); const [canScrollPrev, setCanScrollPrev] = useState(false); @@ -24,76 +33,59 @@ export function CategoryCarousel({ category }: { category: ProductTypes }) { const [isVisible, setIsVisible] = useState(false); const sectionRef = useRef(null); - // Intersection Observer - faqat ko'ringan kategoriyalar uchun API so'rov yuborish + // Intersection Observer useEffect(() => { + if (!sectionRef.current) return; + const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setIsVisible(true); - observer.disconnect(); // Bir marta ko'ringandan keyin observer ni o'chirish + observer.disconnect(); } }); }, - { - rootMargin: '100px', // Sahifa 100px yaqinlashganda yuklash - threshold: 0.1, - }, + { rootMargin: '100px', threshold: 0.1 }, ); - if (sectionRef.current) { - observer.observe(sectionRef.current); - } - + observer.observe(sectionRef.current); return () => observer.disconnect(); }, []); + // Carousel buttons useEffect(() => { if (!api) return; - const updateButtons = () => { setCanScrollPrev(api.canScrollPrev()); setCanScrollNext(api.canScrollNext()); }; - updateButtons(); api.on('select', updateButtons); api.on('reInit', updateButtons); - return () => { api.off('select', updateButtons); api.off('reInit', updateButtons); }; }, [api]); - const scrollPrev = () => { - if (api) { - api?.scrollPrev(); - } - }; + const scrollPrev = () => api?.scrollPrev(); + const scrollNext = () => api?.scrollNext(); - const scrollNext = () => { - if (api) { - api?.scrollNext(); - } - }; - - // Faqat ko'ringanda API so'rov yuborish + // React Query const { data: product, isLoading } = useQuery({ queryKey: ['product_list', category.id], queryFn: () => product_api.list({ page: 1, page_size: 16, - product_type_id: category.id, + category_id: category.id, }), - select(data) { - return data.data; - }, - enabled: isVisible, // Faqat ko'ringanda ishga tushadi + select: (data) => data.data, + enabled: isVisible, }); - // Agar hali ko'rinmagan bo'lsa, bo'sh div qaytarish (scroll uchun joy) + // Shartli renderlar if (!isVisible) { return (
p.state === 'A'); - if (activeProducts.length === 0) { - return null; - } - } + const activeProducts = product?.results.filter((p) => p.state === 'A') ?? []; + if (!isLoading && activeProducts.length === 0) return null; - if (isLoading) { - return null; - } + if (isLoading) return null; return (
- {product && - !isLoading && - product.results - .filter((product) => product.state === 'A') - .map((product) => ( - - - - ))} + {activeProducts.map((product) => ( + + + + ))} +
); -} +}); + +export default CategoryCarousel; diff --git a/src/widgets/welcome/ui/index.tsx b/src/widgets/welcome/ui/index.tsx index 5e6a6f4..0aa0b23 100644 --- a/src/widgets/welcome/ui/index.tsx +++ b/src/widgets/welcome/ui/index.tsx @@ -16,7 +16,7 @@ import { type CarouselApi, } from '@/shared/ui/carousel'; import { Skeleton } from '@/shared/ui/skeleton'; -import { CategoryCarousel } from '@/widgets/categories/ui/category-carousel'; +import CategoryCarousel from '@/widgets/categories/ui/category-carousel'; import { ProductCard } from '@/widgets/categories/ui/product-card'; import { useQuery } from '@tanstack/react-query'; import { AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react'; @@ -241,7 +241,7 @@ const Welcome = () => { ))} - {' '} +
{category && - category.map((e) => - e.product_types.map((c) => ( - - )), - )} + category.map((e) => )} ); };