Merge version_2 into main #9
@@ -1,18 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useDepth3DAnimation } from './useDepth3DAnimation';
|
||||
|
||||
interface CardAnimationConfig {
|
||||
isAnimating?: boolean;
|
||||
transform?: string;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
easing?: string;
|
||||
itemRefs?: React.MutableRefObject<HTMLElement | null>[];
|
||||
containerRef?: React.MutableRefObject<HTMLElement | null>;
|
||||
perspectiveRef?: React.MutableRefObject<HTMLElement | null>;
|
||||
bottomContentRef?: React.MutableRefObject<HTMLElement | null>;
|
||||
}
|
||||
|
||||
const useCardAnimation = (config: CardAnimationConfig = {}) => {
|
||||
const useCardAnimation = (config: CardAnimationConfig = {}): CardAnimationConfig => {
|
||||
const { duration = 0.6, delay = 0, easing = 'ease-out' } = config;
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const { transform } = useDepth3DAnimation({ rotateX: 0, rotateY: 0, scale: 1 });
|
||||
const itemRefs = useRef<HTMLElement | null>(null);
|
||||
const containerRef = useRef<HTMLElement | null>(null);
|
||||
const perspectiveRef = useRef<HTMLElement | null>(null);
|
||||
const bottomContentRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsAnimating(true);
|
||||
@@ -24,6 +34,10 @@ const useCardAnimation = (config: CardAnimationConfig = {}) => {
|
||||
duration,
|
||||
delay,
|
||||
easing,
|
||||
itemRefs: [itemRefs],
|
||||
containerRef,
|
||||
perspectiveRef,
|
||||
bottomContentRef,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,156 +1,62 @@
|
||||
"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 } from 'react';
|
||||
import { useProducts } from '@/hooks/useProducts';
|
||||
import ProductCatalogItem from './ProductCatalogItem';
|
||||
|
||||
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;
|
||||
className?: string;
|
||||
gridClassName?: string;
|
||||
itemClassName?: string;
|
||||
ariaLabel?: 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();
|
||||
const ProductCatalog: React.FC<ProductCatalogProps> = ({
|
||||
className = '',
|
||||
gridClassName = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
|
||||
itemClassName = '',
|
||||
ariaLabel = 'Product catalog',
|
||||
}) => {
|
||||
const { products, loading, error } = useProducts();
|
||||
const [favorites, setFavorites] = useState<Set<string>>(new Set());
|
||||
|
||||
const handleProductClick = useCallback((productId: string) => {
|
||||
router.push(`/shop/${productId}`);
|
||||
}, [router]);
|
||||
const handleFavorite = (productId: string) => {
|
||||
setFavorites((prev) => {
|
||||
const updated = new Set(prev);
|
||||
if (updated.has(productId)) {
|
||||
updated.delete(productId);
|
||||
} else {
|
||||
updated.add(productId);
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
const products: CatalogProduct[] = useMemo(() => {
|
||||
if (productsProp && productsProp.length > 0) {
|
||||
return productsProp;
|
||||
}
|
||||
if (loading) return <div className={className}>Loading products...</div>;
|
||||
if (error) return <div className={className}>Error: {error}</div>;
|
||||
|
||||
if (fetchedProducts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
if (isLoading && (!productsProp || productsProp.length === 0)) {
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative w-content-width mx-auto",
|
||||
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<p className="text-sm text-foreground/50 text-center py-20">
|
||||
Loading products...
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cls(
|
||||
"relative w-content-width mx-auto",
|
||||
layout === "page" ? "pt-hero-page-padding pb-20" : "py-20",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{(onSearchChange || (filters && filters.length > 0)) && (
|
||||
<div
|
||||
className={cls(
|
||||
"flex flex-col md:flex-row gap-4 md:items-end mb-6",
|
||||
toolbarClassName
|
||||
)}
|
||||
>
|
||||
{onSearchChange && (
|
||||
<Input
|
||||
value={searchValue}
|
||||
onChange={onSearchChange}
|
||||
placeholder={searchPlaceholder}
|
||||
ariaLabel={searchPlaceholder}
|
||||
className={cls("flex-1 w-full h-9 text-sm", searchClassName)}
|
||||
/>
|
||||
)}
|
||||
{filters && filters.length > 0 && (
|
||||
<div className="flex gap-4 items-end">
|
||||
{filters.map((filter) => (
|
||||
<ProductDetailVariantSelect
|
||||
key={filter.label}
|
||||
variant={filter}
|
||||
selectClassName={filterClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{products.length === 0 ? (
|
||||
<p className="text-sm text-foreground/50 text-center py-20">
|
||||
{emptyMessage}
|
||||
</p>
|
||||
) : (
|
||||
<div
|
||||
className={cls(
|
||||
"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6",
|
||||
gridClassName
|
||||
)}
|
||||
>
|
||||
{products.map((product) => (
|
||||
<ProductCatalogItem
|
||||
key={product.id}
|
||||
product={product}
|
||||
className={cardClassName}
|
||||
imageClassName={imageClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
return (
|
||||
<div className={className} aria-label={ariaLabel}>
|
||||
<div className={gridClassName}>
|
||||
{products.map((product) => (
|
||||
<ProductCatalogItem
|
||||
key={product.id}
|
||||
product={{
|
||||
id: product.id,
|
||||
category: 'General',
|
||||
name: product.name,
|
||||
price: `$${product.price.toFixed(2)}`,
|
||||
rating: product.rating,
|
||||
imageSrc: product.imageSrc,
|
||||
onFavorite: () => handleFavorite(product.id),
|
||||
isFavorited: favorites.has(product.id),
|
||||
}}
|
||||
className={itemClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductCatalog.displayName = "ProductCatalog";
|
||||
|
||||
export default memo(ProductCatalog);
|
||||
export default ProductCatalog;
|
||||
@@ -27,4 +27,5 @@ const useProducts = () => {
|
||||
return { products, loading, error };
|
||||
};
|
||||
|
||||
export { useProducts };
|
||||
export default useProducts;
|
||||
Reference in New Issue
Block a user