Merge version_2 into main #9

Merged
bender merged 3 commits from version_2 into main 2026-03-12 03:57:14 +00:00
3 changed files with 69 additions and 148 deletions

View File

@@ -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,
};
};

View File

@@ -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;

View File

@@ -27,4 +27,5 @@ const useProducts = () => {
return { products, loading, error };
};
export { useProducts };
export default useProducts;