From 6246194508b5ce84ca853eaf90edfd199e7bffea Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:51 +0000 Subject: [PATCH 1/8] Update src/components/cardStack/CardStack.tsx --- src/components/cardStack/CardStack.tsx | 304 +++++++------------------ 1 file changed, 83 insertions(+), 221 deletions(-) diff --git a/src/components/cardStack/CardStack.tsx b/src/components/cardStack/CardStack.tsx index 3003a8a..a57aa4c 100644 --- a/src/components/cardStack/CardStack.tsx +++ b/src/components/cardStack/CardStack.tsx @@ -1,229 +1,91 @@ -"use client"; +import React, { useRef, useEffect, useState } from 'react'; +import { cn } from '@/lib/utils'; +import TimelineBase from './layouts/timelines/TimelineBase'; -import { memo, Children } from "react"; -import { CardStackProps } from "./types"; -import GridLayout from "./layouts/grid/GridLayout"; -import AutoCarousel from "./layouts/carousels/AutoCarousel"; -import ButtonCarousel from "./layouts/carousels/ButtonCarousel"; -import TimelineBase from "./layouts/timelines/TimelineBase"; -import { gridConfigs } from "./layouts/grid/gridConfigs"; +interface CardStackProps { + children: React.ReactNode; + mode?: 'auto' | 'buttons'; + gridVariant?: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid' | 'one-large-right-three-stacked-left' | 'items-top-row-full-width-bottom' | 'full-width-top-items-bottom-row' | 'one-large-left-three-stacked-right' | 'two-items-per-row' | 'timeline'; + uniformGridCustomHeightClasses?: string; + gridRowsClassName?: string; + itemHeightClassesOverride?: Array; + animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal' | 'depth-3d'; + supports3DAnimation?: boolean; + carouselThreshold?: number; + bottomContent?: React.ReactNode; + title?: string; + titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>; + description?: string; + tag?: string; + tagIcon?: React.ComponentType; + tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + buttons?: Array<{ text: string; onClick?: () => void; href?: string }>; + buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground?: boolean; + ariaLabel?: string; + className?: string; + containerClassName?: string; + gridClassName?: string; + carouselClassName?: string; + carouselItemClassName?: string; + controlsClassName?: string; + textBoxClassName?: string; + titleClassName?: string; + titleImageWrapperClassName?: string; + titleImageClassName?: string; + descriptionClassName?: string; + tagClassName?: string; + buttonContainerClassName?: string; + buttonClassName?: string; + buttonTextClassName?: string; +} -const CardStack = ({ - children, - mode = "buttons", - gridVariant = "uniform-all-items-equal", - uniformGridCustomHeightClasses, - gridRowsClassName, - itemHeightClassesOverride, - animationType, - supports3DAnimation = false, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout = "default", - useInvertedBackground, - carouselThreshold = 5, - bottomContent, - className = "", - containerClassName = "", - gridClassName = "", - carouselClassName = "", - carouselItemClassName = "", - controlsClassName = "", - textBoxClassName = "", - titleClassName = "", - titleImageWrapperClassName = "", - titleImageClassName = "", - descriptionClassName = "", - tagClassName = "", - buttonContainerClassName = "", - buttonClassName = "", - buttonTextClassName = "", - ariaLabel = "Card stack", -}: CardStackProps) => { - const childrenArray = Children.toArray(children); - const itemCount = childrenArray.length; +export const CardStack: React.FC = ({ + children, + mode = 'buttons', + gridVariant = 'uniform-all-items-equal', + animationType, + textboxLayout, + ariaLabel = 'Card stack', + className, + containerClassName, + ...props +}) => { + const containerRef = useRef(null); + const [itemCount, setItemCount] = useState(0); - // Check if the current grid config has gridRows defined - const gridConfig = gridConfigs[gridVariant]?.[itemCount]; - const hasFixedGridRows = gridConfig && 'gridRows' in gridConfig && gridConfig.gridRows; - - // If grid has fixed row heights and we have uniformGridCustomHeightClasses, - // we need to use min-h-0 on md+ to prevent conflicts - let adjustedHeightClasses = uniformGridCustomHeightClasses; - if (hasFixedGridRows && uniformGridCustomHeightClasses) { - // Extract the mobile min-height and add md:min-h-0 - const mobileMinHeight = uniformGridCustomHeightClasses.split(' ')[0]; - adjustedHeightClasses = `${mobileMinHeight} md:min-h-0`; + useEffect(() => { + if (containerRef.current) { + const cards = containerRef.current.querySelectorAll('[data-card-item]'); + setItemCount(cards.length); } + }, [children]); - // Timeline layout for zigzag pattern (works best with 3-6 items) - if (gridVariant === "timeline" && itemCount >= 3 && itemCount <= 6) { - // Convert depth-3d to scale-rotate for timeline (doesn't support 3D) - const timelineAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType; + const isTimeline = gridVariant === 'timeline' && itemCount >= 3 && itemCount <= 6; + const isCarousel = itemCount >= (props.carouselThreshold || 5); - return ( - - {childrenArray} - - ); - } - - // Use grid for items below threshold, carousel for items at or above threshold - // Timeline with 7+ items will also use carousel - const useCarousel = itemCount >= carouselThreshold || (gridVariant === "timeline" && itemCount > 6); - - // Grid layout for 1-4 items - if (!useCarousel) { - return ( - - {childrenArray} - - ); - } - - // Auto-scroll carousel for 5+ items - if (mode === "auto") { - // Convert depth-3d to scale-rotate for carousel (doesn't support 3D) - const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType; - - return ( - - {childrenArray} - - ); - } - - // Button-controlled carousel for 5+ items - // Convert depth-3d to scale-rotate for carousel (doesn't support 3D) - const carouselAnimationType = animationType === "depth-3d" ? "scale-rotate" : animationType; - - return ( - - {childrenArray} - - ); + return ( +
+ {isTimeline ? ( + + {children} + + ) : isCarousel && mode === 'buttons' ? ( +
+ {children} +
+ ) : ( +
+ {children} +
+ )} +
+ ); }; -CardStack.displayName = "CardStack"; - -export default memo(CardStack); +export default CardStack; -- 2.49.1 From f9dd6d6b3ef877ce619ab998efad469602b4a7bb Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:52 +0000 Subject: [PATCH 2/8] Update src/components/cardStack/hooks/useCardAnimation.ts --- .../cardStack/hooks/useCardAnimation.ts | 203 +++--------------- 1 file changed, 35 insertions(+), 168 deletions(-) diff --git a/src/components/cardStack/hooks/useCardAnimation.ts b/src/components/cardStack/hooks/useCardAnimation.ts index 4331477..719b31a 100644 --- a/src/components/cardStack/hooks/useCardAnimation.ts +++ b/src/components/cardStack/hooks/useCardAnimation.ts @@ -1,187 +1,54 @@ -import { useRef } from "react"; -import { useGSAP } from "@gsap/react"; -import gsap from "gsap"; -import { ScrollTrigger } from "gsap/ScrollTrigger"; -import type { CardAnimationType, GridVariant } from "../types"; -import { useDepth3DAnimation } from "./useDepth3DAnimation"; +'use client'; + +import { useEffect, useRef } from 'react'; +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; gsap.registerPlugin(ScrollTrigger); -interface UseCardAnimationProps { - animationType: CardAnimationType | "depth-3d"; - itemCount: number; - isGrid?: boolean; - supports3DAnimation?: boolean; - gridVariant?: GridVariant; - useIndividualTriggers?: boolean; +interface UseCardAnimationReturn { + isMobile: boolean; + itemRefs: React.RefObject[]; } -export const useCardAnimation = ({ - animationType, - itemCount, - isGrid = true, - supports3DAnimation = false, - gridVariant, - useIndividualTriggers = false -}: UseCardAnimationProps) => { - const itemRefs = useRef<(HTMLElement | null)[]>([]); - const containerRef = useRef(null); - const perspectiveRef = useRef(null); - const bottomContentRef = useRef(null); +export const useCardAnimation = (itemCount: number): UseCardAnimationReturn => { + const itemRefs = useRef[]>([]); + const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; - // Enable 3D effect only when explicitly supported and conditions are met - const { isMobile } = useDepth3DAnimation({ - itemRefs, - containerRef, - perspectiveRef, - isEnabled: animationType === "depth-3d" && isGrid && supports3DAnimation && gridVariant === "uniform-all-items-equal", - }); - - // Use scale-rotate as fallback when depth-3d conditions aren't met - const effectiveAnimationType = - animationType === "depth-3d" && (isMobile || !isGrid || gridVariant !== "uniform-all-items-equal") - ? "scale-rotate" - : animationType; - - useGSAP(() => { - if (effectiveAnimationType === "none" || effectiveAnimationType === "depth-3d" || itemRefs.current.length === 0) return; - - const items = itemRefs.current.filter((el) => el !== null); - // Include bottomContent in animation if it exists - if (bottomContentRef.current) { - items.push(bottomContentRef.current); + useEffect(() => { + itemRefs.current = itemRefs.current.slice(0, itemCount); + for (let i = itemRefs.current.length; i < itemCount; i++) { + itemRefs.current[i] = { current: null }; } + }, [itemCount]); - if (effectiveAnimationType === "opacity") { - if (useIndividualTriggers) { - items.forEach((item) => { + useEffect(() => { + if (!isMobile) { + itemRefs.current.forEach((ref, index) => { + if (ref.current) { gsap.fromTo( - item, - { opacity: 0 }, + ref.current, + { opacity: 0, y: 20 }, { opacity: 1, - duration: 1.25, - ease: "sine", + y: 0, + duration: 0.6, + delay: index * 0.1, scrollTrigger: { - trigger: item, - start: "top 80%", - toggleActions: "play none none none", + trigger: ref.current, + start: 'top 80%', + end: 'top 20%', + scrub: false, }, } ); - }); - } else { - gsap.fromTo( - items, - { opacity: 0 }, - { - opacity: 1, - duration: 1.25, - stagger: 0.15, - ease: "sine", - scrollTrigger: { - trigger: items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - } - } else if (effectiveAnimationType === "slide-up") { - items.forEach((item, index) => { - gsap.fromTo( - item, - { opacity: 0, yPercent: 15 }, - { - opacity: 1, - yPercent: 0, - duration: 1, - delay: useIndividualTriggers ? 0 : index * 0.15, - ease: "sine", - scrollTrigger: { - trigger: useIndividualTriggers ? item : items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); + } }); - } else if (effectiveAnimationType === "scale-rotate") { - if (useIndividualTriggers) { - items.forEach((item) => { - gsap.fromTo( - item, - { scaleX: 0, rotate: 10 }, - { - scaleX: 1, - rotate: 0, - duration: 1, - ease: "power3", - scrollTrigger: { - trigger: item, - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - }); - } else { - gsap.fromTo( - items, - { scaleX: 0, rotate: 10 }, - { - scaleX: 1, - rotate: 0, - duration: 1, - stagger: 0.15, - ease: "power3", - scrollTrigger: { - trigger: items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - } - } else if (effectiveAnimationType === "blur-reveal") { - if (useIndividualTriggers) { - items.forEach((item) => { - gsap.fromTo( - item, - { opacity: 0, filter: "blur(10px)" }, - { - opacity: 1, - filter: "blur(0px)", - duration: 1.2, - ease: "power2.out", - scrollTrigger: { - trigger: item, - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - }); - } else { - gsap.fromTo( - items, - { opacity: 0, filter: "blur(10px)" }, - { - opacity: 1, - filter: "blur(0px)", - duration: 1.2, - stagger: 0.15, - ease: "power2.out", - scrollTrigger: { - trigger: items[0], - start: "top 80%", - toggleActions: "play none none none", - }, - } - ); - } } - }, [effectiveAnimationType, itemCount, useIndividualTriggers]); + }, [isMobile]); - return { itemRefs, containerRef, perspectiveRef, bottomContentRef }; + return { + isMobile, + itemRefs: itemRefs.current, + }; }; -- 2.49.1 From 686546d6936acc246d20255f664d90b40000a346 Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:52 +0000 Subject: [PATCH 3/8] Update src/components/ecommerce/productCatalog/ProductCatalog.tsx --- .../productCatalog/ProductCatalog.tsx | 215 ++++++------------ 1 file changed, 71 insertions(+), 144 deletions(-) diff --git a/src/components/ecommerce/productCatalog/ProductCatalog.tsx b/src/components/ecommerce/productCatalog/ProductCatalog.tsx index fc04961..9b35f76 100644 --- a/src/components/ecommerce/productCatalog/ProductCatalog.tsx +++ b/src/components/ecommerce/productCatalog/ProductCatalog.tsx @@ -1,156 +1,83 @@ -"use client"; +'use client'; -import { memo, useMemo, useCallback } from "react"; -import { useRouter } from "next/navigation"; -import Input from "@/components/form/Input"; -import ProductDetailVariantSelect from "@/components/ecommerce/productDetail/ProductDetailVariantSelect"; -import type { ProductVariant } from "@/components/ecommerce/productDetail/ProductDetailCard"; -import { cls } from "@/lib/utils"; -import { useProducts } from "@/hooks/useProducts"; -import ProductCatalogItem from "./ProductCatalogItem"; -import type { CatalogProduct } from "./ProductCatalogItem"; +import React, { useState, useCallback } from 'react'; +import { cn } from '@/lib/utils'; -interface ProductCatalogProps { - layout: "page" | "section"; - products?: CatalogProduct[]; - searchValue?: string; - onSearchChange?: (value: string) => void; - searchPlaceholder?: string; - filters?: ProductVariant[]; - emptyMessage?: string; - className?: string; - gridClassName?: string; - cardClassName?: string; - imageClassName?: string; - searchClassName?: string; - filterClassName?: string; - toolbarClassName?: string; +export interface CatalogProduct { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt: string; + rating: number; + reviewCount: string; + category: string; } -const ProductCatalog = ({ - layout, - products: productsProp, - searchValue = "", - onSearchChange, - searchPlaceholder = "Search products...", - filters, - emptyMessage = "No products found", - className = "", - gridClassName = "", - cardClassName = "", - imageClassName = "", - searchClassName = "", - filterClassName = "", - toolbarClassName = "", -}: ProductCatalogProps) => { - const router = useRouter(); - const { products: fetchedProducts, isLoading } = useProducts(); +interface ProductCatalogProps { + products?: CatalogProduct[]; + className?: string; + onProductClick?: (productId: string) => void; +} - const handleProductClick = useCallback((productId: string) => { - router.push(`/shop/${productId}`); - }, [router]); +export const ProductCatalog: React.FC = ({ + products = [], + className, + onProductClick, +}) => { + const [selectedCategory, setSelectedCategory] = useState(null); - const products: CatalogProduct[] = useMemo(() => { - if (productsProp && productsProp.length > 0) { - return productsProp; - } + const filteredProducts = selectedCategory + ? products.filter((p) => p.category === selectedCategory) + : products; - if (fetchedProducts.length === 0) { - return []; - } + const handleProductClick = useCallback( + (productId: string) => { + onProductClick?.(productId); + }, + [onProductClick] + ); - return fetchedProducts.map((product) => ({ - id: product.id, - name: product.name, - price: product.price, - imageSrc: product.imageSrc, - imageAlt: product.imageAlt || product.name, - rating: product.rating || 0, - reviewCount: product.reviewCount, - category: product.brand, - onProductClick: () => handleProductClick(product.id), - })); - }, [productsProp, fetchedProducts, handleProductClick]); + const categories = Array.from(new Set(products.map((p) => p.category))); - if (isLoading && (!productsProp || productsProp.length === 0)) { - return ( -
+ {categories.length > 0 && ( +
+ + {categories.map((category) => ( +
- ); - } - - return ( -
- {(onSearchChange || (filters && filters.length > 0)) && ( -
- {onSearchChange && ( - - )} - {filters && filters.length > 0 && ( -
- {filters.map((filter) => ( - - ))} -
- )} -
- )} - - {products.length === 0 ? ( -

- {emptyMessage} -

- ) : ( -
- {products.map((product) => ( - - ))} -
- )} -
- ); + {category} + + ))} + + )} +
+ {filteredProducts.map((product) => ( +
handleProductClick(product.id)} + > + {product.imageAlt} +

{product.name}

+
{'★'.repeat(product.rating)}
+

({product.reviewCount} reviews)

+

{product.price}

+
+ ))} +
+ + ); }; -ProductCatalog.displayName = "ProductCatalog"; - -export default memo(ProductCatalog); \ No newline at end of file +export default ProductCatalog; -- 2.49.1 From 33557801c9a581103e1a54d9572ee7c8d1af0c53 Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:52 +0000 Subject: [PATCH 4/8] Update src/components/sections/product/ProductCardFour.tsx --- .../sections/product/ProductCardFour.tsx | 295 +++++++----------- 1 file changed, 105 insertions(+), 190 deletions(-) diff --git a/src/components/sections/product/ProductCardFour.tsx b/src/components/sections/product/ProductCardFour.tsx index 303ff14..6569c85 100644 --- a/src/components/sections/product/ProductCardFour.tsx +++ b/src/components/sections/product/ProductCardFour.tsx @@ -1,39 +1,38 @@ -"use client"; +'use client'; -import { memo, useCallback } from "react"; -import { useRouter } from "next/navigation"; -import CardStack from "@/components/cardStack/CardStack"; -import ProductImage from "@/components/shared/ProductImage"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import { useProducts } from "@/hooks/useProducts"; -import type { Product } from "@/lib/api/product"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; +import React, { useState } from 'react'; +import { Heart, ArrowRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; -type ProductCardFourGridVariant = Exclude; - -type ProductCard = Product & { - variant: string; -}; +export interface ProductCard { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; + rating: number; + reviewCount: string; + isFavorited?: boolean; + onFavorite?: () => void; + onProductClick?: () => void; +} interface ProductCardFourProps { products?: ProductCard[]; - carouselMode?: "auto" | "buttons"; - gridVariant: ProductCardFourGridVariant; + carouselMode?: 'auto' | 'buttons'; + gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid'; + animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; title: string; - titleSegments?: TitleSegment[]; + titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>; description: string; tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; + tagIcon?: React.ComponentType; + tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + buttons?: Array<{ text: string; onClick?: () => void; href?: string }>; + buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground: boolean; ariaLabel?: string; className?: string; containerClassName?: string; @@ -43,9 +42,10 @@ interface ProductCardFourProps { textBoxTitleImageWrapperClassName?: string; textBoxTitleImageClassName?: string; textBoxDescriptionClassName?: string; + cardBrandClassName?: string; cardNameClassName?: string; cardPriceClassName?: string; - cardVariantClassName?: string; + cardRatingClassName?: string; actionButtonClassName?: string; gridClassName?: string; carouselClassName?: string; @@ -57,182 +57,97 @@ interface ProductCardFourProps { textBoxButtonTextClassName?: string; } -interface ProductCardItemProps { - product: ProductCard; - shouldUseLightText: boolean; - cardClassName?: string; - imageClassName?: string; - cardNameClassName?: string; - cardPriceClassName?: string; - cardVariantClassName?: string; - actionButtonClassName?: string; -} - -const ProductCardItem = memo(({ - product, - shouldUseLightText, - cardClassName = "", - imageClassName = "", - cardNameClassName = "", - cardPriceClassName = "", - cardVariantClassName = "", - actionButtonClassName = "", -}: ProductCardItemProps) => { - return ( -
- - -
-
-
-

- {product.name} -

-

- {product.variant} -

-
-

- {product.price} -

-
-
-
- ); -}); - -ProductCardItem.displayName = "ProductCardItem"; - -const ProductCardFour = ({ - products: productsProp, - carouselMode = "buttons", +export const ProductCardFour: React.FC = ({ + products = [], gridVariant, - uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", animationType, title, - titleSegments, description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, textboxLayout, useInvertedBackground, - ariaLabel = "Product section", - className = "", - containerClassName = "", - cardClassName = "", - imageClassName = "", - textBoxTitleClassName = "", - textBoxTitleImageWrapperClassName = "", - textBoxTitleImageClassName = "", - textBoxDescriptionClassName = "", - cardNameClassName = "", - cardPriceClassName = "", - cardVariantClassName = "", - actionButtonClassName = "", - gridClassName = "", - carouselClassName = "", - controlsClassName = "", - textBoxClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", -}: ProductCardFourProps) => { - const theme = useTheme(); - const router = useRouter(); - const { products: fetchedProducts, isLoading } = useProducts(); - const isFromApi = fetchedProducts.length > 0; - const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[]; - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); + ariaLabel = 'Product section', + className, + containerClassName, + cardClassName, + imageClassName, +}) => { + const [favorites, setFavorites] = useState>(new Set()); - const handleProductClick = useCallback((product: ProductCard) => { - if (isFromApi) { - router.push(`/shop/${product.id}`); + const toggleFavorite = (productId: string) => { + const newFavorites = new Set(favorites); + if (newFavorites.has(productId)) { + newFavorites.delete(productId); } else { - product.onProductClick?.(); + newFavorites.add(productId); } - }, [isFromApi, router]); + setFavorites(newFavorites); + }; - - if (isLoading && !productsProp) { - return ( -
-

Loading products...

-
- ); - } - - if (!products || products.length === 0) { - return null; - } + const handleProductClick = (product: ProductCard) => { + product.onProductClick?.(); + }; return ( - +
+

{title}

+

{description}

+
- title={title} - titleSegments={titleSegments} - description={description} - tag={tag} - tagIcon={tagIcon} - tagAnimation={tagAnimation} - buttons={buttons} - buttonAnimation={buttonAnimation} - textboxLayout={textboxLayout} - useInvertedBackground={useInvertedBackground} - className={className} - containerClassName={containerClassName} - gridClassName={gridClassName} - carouselClassName={carouselClassName} - controlsClassName={controlsClassName} - textBoxClassName={textBoxClassName} - titleClassName={textBoxTitleClassName} - titleImageWrapperClassName={textBoxTitleImageWrapperClassName} - titleImageClassName={textBoxTitleImageClassName} - descriptionClassName={textBoxDescriptionClassName} - tagClassName={textBoxTagClassName} - buttonContainerClassName={textBoxButtonContainerClassName} - buttonClassName={textBoxButtonClassName} - buttonTextClassName={textBoxButtonTextClassName} - ariaLabel={ariaLabel} - > - {products?.map((product, index) => ( - handleProductClick(product) }} - shouldUseLightText={shouldUseLightText} - cardClassName={cardClassName} - imageClassName={imageClassName} - cardNameClassName={cardNameClassName} - cardPriceClassName={cardPriceClassName} - cardVariantClassName={cardVariantClassName} - actionButtonClassName={actionButtonClassName} - /> - ))} -
+
+ {products.map((product) => ( +
+
+ {product.imageAlt + +
+
+

{product.name}

+
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ★ + + ))} +
+ ({product.reviewCount}) +
+

{product.price}

+ +
+
+ ))} +
+ ); }; -ProductCardFour.displayName = "ProductCardFour"; - export default ProductCardFour; -- 2.49.1 From 7b6f8796915613a7c149c933c4903d7837c58281 Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:53 +0000 Subject: [PATCH 5/8] Update src/components/sections/product/ProductCardOne.tsx --- .../sections/product/ProductCardOne.tsx | 340 +++++++----------- 1 file changed, 130 insertions(+), 210 deletions(-) diff --git a/src/components/sections/product/ProductCardOne.tsx b/src/components/sections/product/ProductCardOne.tsx index 15537bc..e704052 100644 --- a/src/components/sections/product/ProductCardOne.tsx +++ b/src/components/sections/product/ProductCardOne.tsx @@ -1,226 +1,146 @@ -"use client"; +'use client'; -import { memo, useCallback } from "react"; -import { useRouter } from "next/navigation"; -import { ArrowUpRight } from "lucide-react"; -import CardStack from "@/components/cardStack/CardStack"; -import ProductImage from "@/components/shared/ProductImage"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import { useProducts } from "@/hooks/useProducts"; -import type { Product } from "@/lib/api/product"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; +import React, { useState, useRef } from 'react'; +import { Heart } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; -type ProductCardOneGridVariant = Exclude; +gsap.registerPlugin(ScrollTrigger); -type ProductCard = Product; +export interface Product { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; + isFavorited?: boolean; + onFavorite?: () => void; + onProductClick?: () => void; +} interface ProductCardOneProps { - products?: ProductCard[]; - carouselMode?: "auto" | "buttons"; - gridVariant: ProductCardOneGridVariant; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; - title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; - ariaLabel?: string; - className?: string; - containerClassName?: string; - cardClassName?: string; - imageClassName?: string; - textBoxTitleClassName?: string; - textBoxTitleImageWrapperClassName?: string; - textBoxTitleImageClassName?: string; - textBoxDescriptionClassName?: string; - cardNameClassName?: string; - cardPriceClassName?: string; - gridClassName?: string; - carouselClassName?: string; - controlsClassName?: string; - textBoxClassName?: string; - textBoxTagClassName?: string; - textBoxButtonContainerClassName?: string; - textBoxButtonClassName?: string; - textBoxButtonTextClassName?: string; + products?: Product[]; + carouselMode?: 'auto' | 'buttons'; + gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid'; + animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; + uniformGridCustomHeightClasses?: string; + title: string; + titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>; + description: string; + tag?: string; + tagIcon?: React.ComponentType; + tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + buttons?: Array<{ text: string; onClick?: () => void; href?: string }>; + buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground: boolean; + ariaLabel?: string; + className?: string; + containerClassName?: string; + cardClassName?: string; + imageClassName?: string; + textBoxTitleClassName?: string; + textBoxTitleImageWrapperClassName?: string; + textBoxTitleImageClassName?: string; + textBoxDescriptionClassName?: string; + cardBrandClassName?: string; + cardNameClassName?: string; + cardPriceClassName?: string; + cardRatingClassName?: string; + actionButtonClassName?: string; + gridClassName?: string; + carouselClassName?: string; + controlsClassName?: string; + textBoxClassName?: string; + textBoxTagClassName?: string; + textBoxButtonContainerClassName?: string; + textBoxButtonClassName?: string; + textBoxButtonTextClassName?: string; } -interface ProductCardItemProps { - product: ProductCard; - shouldUseLightText: boolean; - cardClassName?: string; - imageClassName?: string; - cardNameClassName?: string; - cardPriceClassName?: string; -} +export const ProductCardOne: React.FC = ({ + products = [], + gridVariant, + animationType, + title, + description, + textboxLayout, + useInvertedBackground, + ariaLabel = 'Product section', + className, + containerClassName, + cardClassName, + imageClassName, +}) => { + const [favorites, setFavorites] = useState>(new Set()); + const containerRef = useRef(null); -const ProductCardItem = memo(({ - product, - shouldUseLightText, - cardClassName = "", - imageClassName = "", - cardNameClassName = "", - cardPriceClassName = "", -}: ProductCardItemProps) => { - return ( -
- - -
-
-

- {product.name} -

-

- {product.price} -

-
- - -
-
- ); -}); - -ProductCardItem.displayName = "ProductCardItem"; - -const ProductCardOne = ({ - products: productsProp, - carouselMode = "buttons", - gridVariant, - uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - useInvertedBackground, - ariaLabel = "Product section", - className = "", - containerClassName = "", - cardClassName = "", - imageClassName = "", - textBoxTitleClassName = "", - textBoxTitleImageWrapperClassName = "", - textBoxTitleImageClassName = "", - textBoxDescriptionClassName = "", - cardNameClassName = "", - cardPriceClassName = "", - gridClassName = "", - carouselClassName = "", - controlsClassName = "", - textBoxClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", -}: ProductCardOneProps) => { - const theme = useTheme(); - const router = useRouter(); - const { products: fetchedProducts, isLoading } = useProducts(); - const isFromApi = fetchedProducts.length > 0; - const products = isFromApi ? fetchedProducts : productsProp; - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - - const handleProductClick = useCallback((product: ProductCard) => { - if (isFromApi) { - router.push(`/shop/${product.id}`); - } else { - product.onProductClick?.(); - } - }, [isFromApi, router]); - - if (isLoading && !productsProp) { - return ( -
-

Loading products...

-
- ); + const toggleFavorite = (productId: string) => { + const newFavorites = new Set(favorites); + if (newFavorites.has(productId)) { + newFavorites.delete(productId); + } else { + newFavorites.add(productId); } + setFavorites(newFavorites); + }; - if (!products || products.length === 0) { - return null; - } + const handleProductClick = (product: Product) => { + product.onProductClick?.(); + }; - return ( - +
+

{title}

+

{description}

+
- title={title} - titleSegments={titleSegments} - description={description} - tag={tag} - tagIcon={tagIcon} - tagAnimation={tagAnimation} - buttons={buttons} - buttonAnimation={buttonAnimation} - textboxLayout={textboxLayout} - useInvertedBackground={useInvertedBackground} - className={className} - containerClassName={containerClassName} - gridClassName={gridClassName} - carouselClassName={carouselClassName} - controlsClassName={controlsClassName} - textBoxClassName={textBoxClassName} - titleClassName={textBoxTitleClassName} - titleImageWrapperClassName={textBoxTitleImageWrapperClassName} - titleImageClassName={textBoxTitleImageClassName} - descriptionClassName={textBoxDescriptionClassName} - tagClassName={textBoxTagClassName} - buttonContainerClassName={textBoxButtonContainerClassName} - buttonClassName={textBoxButtonClassName} - buttonTextClassName={textBoxButtonTextClassName} - ariaLabel={ariaLabel} - > - {products?.map((product, index) => ( - handleProductClick(product) }} - shouldUseLightText={shouldUseLightText} - cardClassName={cardClassName} - imageClassName={imageClassName} - cardNameClassName={cardNameClassName} - cardPriceClassName={cardPriceClassName} +
+ {products.map((product) => ( +
+
+ {product.imageAlt + +
+
+

{product.name}

+

{product.price}

+ +
+
+ ))} +
+ + ); }; -ProductCardOne.displayName = "ProductCardOne"; - export default ProductCardOne; -- 2.49.1 From 57fb85c4447f7fccf6b4d5725019932786f65b14 Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:53 +0000 Subject: [PATCH 6/8] Update src/components/sections/product/ProductCardThree.tsx --- .../sections/product/ProductCardThree.tsx | 405 ++++++------------ 1 file changed, 139 insertions(+), 266 deletions(-) diff --git a/src/components/sections/product/ProductCardThree.tsx b/src/components/sections/product/ProductCardThree.tsx index f53d136..bd3a5a2 100644 --- a/src/components/sections/product/ProductCardThree.tsx +++ b/src/components/sections/product/ProductCardThree.tsx @@ -1,283 +1,156 @@ -"use client"; +'use client'; -import { memo, useState, useCallback } from "react"; -import { useRouter } from "next/navigation"; -import { Plus, Minus } from "lucide-react"; -import CardStack from "@/components/cardStack/CardStack"; -import ProductImage from "@/components/shared/ProductImage"; -import QuantityButton from "@/components/shared/QuantityButton"; -import Button from "@/components/button/Button"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import { useProducts } from "@/hooks/useProducts"; -import { getButtonProps } from "@/lib/buttonUtils"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import type { Product } from "@/lib/api/product"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, ButtonAnimationType, GridVariant, CardAnimationType, TitleSegment } from "@/components/cardStack/types"; -import type { CTAButtonVariant, ButtonPropsForVariant } from "@/components/button/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; +import React, { useState } from 'react'; +import { Heart, ShoppingCart } from 'lucide-react'; +import { cn } from '@/lib/utils'; -type ProductCardThreeGridVariant = Exclude; - -type ProductCard = Product & { - onQuantityChange?: (quantity: number) => void; - initialQuantity?: number; - priceButtonProps?: Partial>; -}; +export interface ProductCard { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; + rating: number; + reviewCount: string; + isFavorited?: boolean; + onFavorite?: () => void; + onProductClick?: () => void; +} interface ProductCardThreeProps { - products?: ProductCard[]; - carouselMode?: "auto" | "buttons"; - gridVariant: ProductCardThreeGridVariant; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; - title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; - ariaLabel?: string; - className?: string; - containerClassName?: string; - cardClassName?: string; - imageClassName?: string; - textBoxTitleClassName?: string; - textBoxTitleImageWrapperClassName?: string; - textBoxTitleImageClassName?: string; - textBoxDescriptionClassName?: string; - cardNameClassName?: string; - quantityControlsClassName?: string; - gridClassName?: string; - carouselClassName?: string; - controlsClassName?: string; - textBoxClassName?: string; - textBoxTagClassName?: string; - textBoxButtonContainerClassName?: string; - textBoxButtonClassName?: string; - textBoxButtonTextClassName?: string; + products?: ProductCard[]; + carouselMode?: 'auto' | 'buttons'; + gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid'; + animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; + uniformGridCustomHeightClasses?: string; + title: string; + titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>; + description: string; + tag?: string; + tagIcon?: React.ComponentType; + tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + buttons?: Array<{ text: string; onClick?: () => void; href?: string }>; + buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground: boolean; + ariaLabel?: string; + className?: string; + containerClassName?: string; + cardClassName?: string; + imageClassName?: string; + textBoxTitleClassName?: string; + textBoxTitleImageWrapperClassName?: string; + textBoxTitleImageClassName?: string; + textBoxDescriptionClassName?: string; + cardBrandClassName?: string; + cardNameClassName?: string; + cardPriceClassName?: string; + cardRatingClassName?: string; + actionButtonClassName?: string; + gridClassName?: string; + carouselClassName?: string; + controlsClassName?: string; + textBoxClassName?: string; + textBoxTagClassName?: string; + textBoxButtonContainerClassName?: string; + textBoxButtonClassName?: string; + textBoxButtonTextClassName?: string; } +export const ProductCardThree: React.FC = ({ + products = [], + gridVariant, + animationType, + title, + description, + textboxLayout, + useInvertedBackground, + ariaLabel = 'Product section', + className, + containerClassName, + cardClassName, + imageClassName, +}) => { + const [favorites, setFavorites] = useState>(new Set()); -interface ProductCardItemProps { - product: ProductCard; - shouldUseLightText: boolean; - isFromApi: boolean; - onBuyClick?: (productId: string, quantity: number) => void; - cardClassName?: string; - imageClassName?: string; - cardNameClassName?: string; - quantityControlsClassName?: string; -} + const toggleFavorite = (productId: string) => { + const newFavorites = new Set(favorites); + if (newFavorites.has(productId)) { + newFavorites.delete(productId); + } else { + newFavorites.add(productId); + } + setFavorites(newFavorites); + }; -const ProductCardItem = memo(({ - product, - shouldUseLightText, - isFromApi, - onBuyClick, - cardClassName = "", - imageClassName = "", - cardNameClassName = "", - quantityControlsClassName = "", -}: ProductCardItemProps) => { - const theme = useTheme(); - const [quantity, setQuantity] = useState(product.initialQuantity || 1); - - const handleIncrement = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - const newQuantity = quantity + 1; - setQuantity(newQuantity); - product.onQuantityChange?.(newQuantity); - }, [quantity, product]); - - const handleDecrement = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - if (quantity > 1) { - const newQuantity = quantity - 1; - setQuantity(newQuantity); - product.onQuantityChange?.(newQuantity); - } - }, [quantity, product]); - - const handleClick = useCallback(() => { - if (isFromApi && onBuyClick) { - onBuyClick(product.id, quantity); - } else { - product.onProductClick?.(); - } - }, [isFromApi, onBuyClick, product, quantity]); + const handleProductClick = (product: ProductCard) => { + product.onProductClick?.(); + }; + const renderStars = (rating: number) => { return ( -
- - -
-

- {product.name} -

- -
-
- - - {quantity} - - -
- -
-
-
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ★ + + ))} +
); -}); + }; -ProductCardItem.displayName = "ProductCardItem"; + return ( +
+
+

{title}

+

{description}

+
-const ProductCardThree = ({ - products: productsProp, - carouselMode = "buttons", - gridVariant, - uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - useInvertedBackground, - ariaLabel = "Product section", - className = "", - containerClassName = "", - cardClassName = "", - imageClassName = "", - textBoxTitleClassName = "", - textBoxTitleImageWrapperClassName = "", - textBoxTitleImageClassName = "", - textBoxDescriptionClassName = "", - cardNameClassName = "", - quantityControlsClassName = "", - gridClassName = "", - carouselClassName = "", - controlsClassName = "", - textBoxClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", -}: ProductCardThreeProps) => { - const theme = useTheme(); - const router = useRouter(); - const { products: fetchedProducts, isLoading } = useProducts(); - const isFromApi = fetchedProducts.length > 0; - const products = (isFromApi ? fetchedProducts : productsProp) as ProductCard[]; - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - - const handleProductClick = useCallback((product: ProductCard) => { - if (isFromApi) { - router.push(`/shop/${product.id}`); - } else { - product.onProductClick?.(); - } - }, [isFromApi, router]); - - if (isLoading && !productsProp) { - return ( -
-

Loading products...

-
- ); - } - - if (!products || products.length === 0) { - return null; - } - - return ( - - {products?.map((product, index) => ( - handleProductClick(product) }} - shouldUseLightText={shouldUseLightText} - isFromApi={isFromApi} - cardClassName={cardClassName} - imageClassName={imageClassName} - cardNameClassName={cardNameClassName} - quantityControlsClassName={quantityControlsClassName} +
+ {products.map((product) => ( +
+
+ {product.imageAlt + +
+
+

{product.name}

+
+ {renderStars(product.rating)} + ({product.reviewCount}) +
+

{product.price}

+ +
+
+ ))} +
+
+ ); }; -ProductCardThree.displayName = "ProductCardThree"; - export default ProductCardThree; -- 2.49.1 From 7e072eff3889d0a976a248d0e7d8269f5b177012 Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:54 +0000 Subject: [PATCH 7/8] Update src/components/sections/product/ProductCardTwo.tsx --- .../sections/product/ProductCardTwo.tsx | 396 +++++++----------- 1 file changed, 144 insertions(+), 252 deletions(-) diff --git a/src/components/sections/product/ProductCardTwo.tsx b/src/components/sections/product/ProductCardTwo.tsx index fe4a562..1094a39 100644 --- a/src/components/sections/product/ProductCardTwo.tsx +++ b/src/components/sections/product/ProductCardTwo.tsx @@ -1,267 +1,159 @@ -"use client"; +'use client'; -import { memo, useCallback } from "react"; -import { useRouter } from "next/navigation"; -import { Star } from "lucide-react"; -import CardStack from "@/components/cardStack/CardStack"; -import ProductImage from "@/components/shared/ProductImage"; -import { cls, shouldUseInvertedText } from "@/lib/utils"; -import { useTheme } from "@/providers/themeProvider/ThemeProvider"; -import { useProducts } from "@/hooks/useProducts"; -import type { Product } from "@/lib/api/product"; -import type { LucideIcon } from "lucide-react"; -import type { ButtonConfig, GridVariant, CardAnimationType, TitleSegment, ButtonAnimationType } from "@/components/cardStack/types"; -import type { TextboxLayout, InvertedBackground } from "@/providers/themeProvider/config/constants"; +import React, { useState } from 'react'; +import { Heart, Star } from 'lucide-react'; +import { cn } from '@/lib/utils'; -type ProductCardTwoGridVariant = Exclude; - -type ProductCard = Product & { - brand: string; - rating: number; - reviewCount: string; -}; +export interface ProductCard { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; + rating: number; + reviewCount: string; + brand: string; + isFavorited?: boolean; + onFavorite?: () => void; + onProductClick?: () => void; +} interface ProductCardTwoProps { - products?: ProductCard[]; - carouselMode?: "auto" | "buttons"; - gridVariant: ProductCardTwoGridVariant; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; - title: string; - titleSegments?: TitleSegment[]; - description: string; - tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; - ariaLabel?: string; - className?: string; - containerClassName?: string; - cardClassName?: string; - imageClassName?: string; - textBoxTitleClassName?: string; - textBoxTitleImageWrapperClassName?: string; - textBoxTitleImageClassName?: string; - textBoxDescriptionClassName?: string; - cardBrandClassName?: string; - cardNameClassName?: string; - cardPriceClassName?: string; - cardRatingClassName?: string; - actionButtonClassName?: string; - gridClassName?: string; - carouselClassName?: string; - controlsClassName?: string; - textBoxClassName?: string; - textBoxTagClassName?: string; - textBoxButtonContainerClassName?: string; - textBoxButtonClassName?: string; - textBoxButtonTextClassName?: string; + products?: ProductCard[]; + carouselMode?: 'auto' | 'buttons'; + gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid'; + animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; + uniformGridCustomHeightClasses?: string; + title: string; + titleSegments?: Array<{ type: 'text'; content: string } | { type: 'image'; src: string; alt?: string }>; + description: string; + tag?: string; + tagIcon?: React.ComponentType; + tagAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + buttons?: Array<{ text: string; onClick?: () => void; href?: string }>; + buttonAnimation?: 'none' | 'opacity' | 'slide-up' | 'blur-reveal'; + textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + useInvertedBackground: boolean; + ariaLabel?: string; + className?: string; + containerClassName?: string; + cardClassName?: string; + imageClassName?: string; + textBoxTitleClassName?: string; + textBoxTitleImageWrapperClassName?: string; + textBoxTitleImageClassName?: string; + textBoxDescriptionClassName?: string; + cardBrandClassName?: string; + cardNameClassName?: string; + cardPriceClassName?: string; + cardRatingClassName?: string; + actionButtonClassName?: string; + gridClassName?: string; + carouselClassName?: string; + controlsClassName?: string; + textBoxClassName?: string; + textBoxTagClassName?: string; + textBoxButtonContainerClassName?: string; + textBoxButtonClassName?: string; + textBoxButtonTextClassName?: string; } -interface ProductCardItemProps { - product: ProductCard; - shouldUseLightText: boolean; - cardClassName?: string; - imageClassName?: string; - cardBrandClassName?: string; - cardNameClassName?: string; - cardPriceClassName?: string; - cardRatingClassName?: string; - actionButtonClassName?: string; -} +export const ProductCardTwo: React.FC = ({ + products = [], + gridVariant, + animationType, + title, + description, + textboxLayout, + useInvertedBackground, + ariaLabel = 'Product section', + className, + containerClassName, + cardClassName, + imageClassName, +}) => { + const [favorites, setFavorites] = useState>(new Set()); -const ProductCardItem = memo(({ - product, - shouldUseLightText, - cardClassName = "", - imageClassName = "", - cardBrandClassName = "", - cardNameClassName = "", - cardPriceClassName = "", - cardRatingClassName = "", - actionButtonClassName = "", -}: ProductCardItemProps) => { + const toggleFavorite = (productId: string) => { + const newFavorites = new Set(favorites); + if (newFavorites.has(productId)) { + newFavorites.delete(productId); + } else { + newFavorites.add(productId); + } + setFavorites(newFavorites); + }; + + const handleProductClick = (product: ProductCard) => { + product.onProductClick?.(); + }; + + const renderStars = (rating: number) => { return ( -
- - -
-

- {product.brand} -

-
-

- {product.name} -

-
-
- {[...Array(5)].map((_, i) => ( - - ))} -
- - ({product.reviewCount}) - -
-
-

- {product.price} -

-
-
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
); -}); + }; -ProductCardItem.displayName = "ProductCardItem"; + return ( +
+
+

{title}

+

{description}

+
-const ProductCardTwo = ({ - products: productsProp, - carouselMode = "buttons", - gridVariant, - uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", - animationType, - title, - titleSegments, - description, - tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, - textboxLayout, - useInvertedBackground, - ariaLabel = "Product section", - className = "", - containerClassName = "", - cardClassName = "", - imageClassName = "", - textBoxTitleClassName = "", - textBoxTitleImageWrapperClassName = "", - textBoxTitleImageClassName = "", - textBoxDescriptionClassName = "", - cardBrandClassName = "", - cardNameClassName = "", - cardPriceClassName = "", - cardRatingClassName = "", - actionButtonClassName = "", - gridClassName = "", - carouselClassName = "", - controlsClassName = "", - textBoxClassName = "", - textBoxTagClassName = "", - textBoxButtonContainerClassName = "", - textBoxButtonClassName = "", - textBoxButtonTextClassName = "", -}: ProductCardTwoProps) => { - const theme = useTheme(); - const router = useRouter(); - const { products: fetchedProducts, isLoading } = useProducts(); - const isFromApi = fetchedProducts.length > 0; - const products = (fetchedProducts.length > 0 ? fetchedProducts : productsProp) as ProductCard[]; - const shouldUseLightText = shouldUseInvertedText(useInvertedBackground, theme.cardStyle); - - const handleProductClick = useCallback((product: ProductCard) => { - if (isFromApi) { - router.push(`/shop/${product.id}`); - } else { - product.onProductClick?.(); - } - }, [isFromApi, router]); - - const customGridRows = (gridVariant === "bento-grid" || gridVariant === "bento-grid-inverted") - ? "md:grid-rows-[22rem_22rem] 2xl:grid-rows-[26rem_26rem]" - : undefined; - - if (isLoading && !productsProp) { - return ( -
-

Loading products...

-
- ); - } - - if (!products || products.length === 0) { - return null; - } - - return ( - - {products?.map((product, index) => ( - handleProductClick(product) }} - shouldUseLightText={shouldUseLightText} - cardClassName={cardClassName} - imageClassName={imageClassName} - cardBrandClassName={cardBrandClassName} - cardNameClassName={cardNameClassName} - cardPriceClassName={cardPriceClassName} - cardRatingClassName={cardRatingClassName} - actionButtonClassName={actionButtonClassName} +
+ {products.map((product) => ( +
+
+ {product.imageAlt + +
+
+

{product.brand}

+

{product.name}

+
+ {renderStars(product.rating)} + ({product.reviewCount}) +
+

{product.price}

+ +
+
+ ))} +
+
+ ); }; -ProductCardTwo.displayName = "ProductCardTwo"; - export default ProductCardTwo; -- 2.49.1 From 981c2bfc47e97c56a244bb28038eff4d2eae3453 Mon Sep 17 00:00:00 2001 From: bender Date: Sun, 8 Mar 2026 21:27:54 +0000 Subject: [PATCH 8/8] Update src/hooks/useProduct.ts --- src/hooks/useProduct.ts | 84 ++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/hooks/useProduct.ts b/src/hooks/useProduct.ts index 3407f3a..43a1649 100644 --- a/src/hooks/useProduct.ts +++ b/src/hooks/useProduct.ts @@ -1,45 +1,45 @@ -"use client"; +'use client'; -import { useEffect, useState } from "react"; -import { Product, fetchProduct } from "@/lib/api/product"; +import { useState, useEffect } from 'react'; +import { fetchProductById } from '@/lib/api/product'; -export function useProduct(productId: string) { - const [product, setProduct] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let isMounted = true; - - async function loadProduct() { - if (!productId) { - setIsLoading(false); - return; - } - - try { - setIsLoading(true); - const data = await fetchProduct(productId); - if (isMounted) { - setProduct(data); - } - } catch (err) { - if (isMounted) { - setError(err instanceof Error ? err : new Error("Failed to fetch product")); - } - } finally { - if (isMounted) { - setIsLoading(false); - } - } - } - - loadProduct(); - - return () => { - isMounted = false; - }; - }, [productId]); - - return { product, isLoading, error }; +interface UseProductState { + product: any | null; + loading: boolean; + error: string | null; } + +export const useProduct = (productId: string) => { + const [state, setState] = useState({ + product: null, + loading: false, + error: null, + }); + + useEffect(() => { + if (!productId) return; + + const fetchProduct = async () => { + setState((prev) => ({ ...prev, loading: true })); + try { + const product = await fetchProductById(productId); + if (!product) throw new Error('Product not found'); + setState((prev) => ({ + ...prev, + loading: false, + product, + })); + } catch (err) { + setState((prev) => ({ + ...prev, + loading: false, + error: err instanceof Error ? err.message : 'Failed to fetch product', + })); + } + }; + + fetchProduct(); + }, [productId]); + + return state; +}; -- 2.49.1