Merge version_3 into main #8

Merged
bender merged 11 commits from version_3 into main 2026-03-08 01:49:15 +00:00
11 changed files with 582 additions and 1542 deletions

View File

@@ -2,246 +2,197 @@
import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarLayoutFloatingOverlay from "@/components/navbar/NavbarLayoutFloatingOverlay/NavbarLayoutFloatingOverlay";
import HeroSplitTestimonial from "@/components/sections/hero/HeroSplitTestimonial";
import MetricSplitMediaAbout from "@/components/sections/about/MetricSplitMediaAbout";
import ProductCardFour from "@/components/sections/product/ProductCardFour";
import FeatureBorderGlow from "@/components/sections/feature/featureBorderGlow/FeatureBorderGlow";
import TestimonialCardTwo from "@/components/sections/testimonial/TestimonialCardTwo";
import ContactSplit from "@/components/sections/contact/ContactSplit";
import FooterSimple from "@/components/sections/footer/FooterSimple";
import { Clock, DollarSign, Heart, Sparkles, Truck, Zap } from "lucide-react";
import HeroSplitTestimonial from '@/components/sections/hero/HeroSplitTestimonial';
import MetricSplitMediaAbout from '@/components/sections/about/MetricSplitMediaAbout';
import ProductCardFour from '@/components/sections/product/ProductCardFour';
import FeatureBorderGlow from '@/components/sections/feature/featureBorderGlow/FeatureBorderGlow';
import TestimonialCardTwo from '@/components/sections/testimonial/TestimonialCardTwo';
import { ContactSplit } from '@/components/sections/contact/ContactSplit';
import FooterSimple from '@/components/sections/footer/FooterSimple';
import { Star, Zap, Shield, TrendingUp, Users, ArrowRight } from 'lucide-react';
export default function LandingPage() {
const navItems = [
{ name: "Home", id: "/" },
{ name: "About", id: "about" },
{ name: "Products", id: "products" },
{ name: "Features", id: "features" },
{ name: "Testimonials", id: "testimonials" },
{ name: "Contact", id: "contact" },
];
const heroTestimonials = [
{
name: "Sarah Johnson", handle: "@sarahjohnson", testimonial: "Amazing products and excellent service!", rating: 5,
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Sarah Johnson"
},
{
name: "Ahmed Hassan", handle: "@ahmedhassan", testimonial: "Best experience I've had!", rating: 5,
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Ahmed Hassan"
},
{
name: "Maria Garcia", handle: "@mariagarcia", testimonial: "Highly recommended for everyone!", rating: 5,
imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Maria Garcia"
}
];
const metrics = [
{ value: "500", title: "Happy Customers" },
{ value: "1000+", title: "Orders Delivered" }
];
const products = [
{
id: "1", name: "Premium Bread", price: "$8.99", variant: "Fresh Baked", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Premium Bread", isFavorited: false
},
{
id: "2", name: "Croissants", price: "$5.99", variant: "Buttery", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Croissants", isFavorited: false
},
{
id: "3", name: "Birthday Cake", price: "$24.99", variant: "Custom", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Birthday Cake", isFavorited: false
},
{
id: "4", name: "Donut Box", price: "$12.99", variant: "Assorted", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Donut Box", isFavorited: false
}
];
const features = [
{ icon: Zap, title: "Fast Delivery", description: "Get your orders delivered quickly" },
{ icon: Shield, title: "Quality Assured", description: "Fresh baked products guaranteed" },
{ icon: TrendingUp, title: "24/7 Available", description: "Always open for your needs" },
{ icon: Users, title: "Customer Support", description: "Dedicated support team ready" }
];
const testimonials = [
{
id: "1", name: "Fatima Al-Mansouri", role: "Business Owner", testimonial: "Al Rayah Bakeries has been our trusted supplier for years. Their quality never disappoints!", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Fatima Al-Mansouri", icon: Star
},
{
id: "2", name: "Mohammed Ahmed", role: "Restaurant Manager", testimonial: "Fresh products delivered on time, every single day. Highly professional!", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Mohammed Ahmed", icon: Star
},
{
id: "3", name: "Layla Khalil", role: "Event Planner", testimonial: "Their custom cakes are absolutely stunning and delicious. Customers love them!", imageSrc: "/placeholders/placeholder1.webp", imageAlt: "Layla Khalil", icon: Star
}
];
const footerColumns = [
{
title: "Quick Links", items: [
{ label: "Home", href: "/" },
{ label: "About", href: "#about" },
{ label: "Contact", href: "#contact" }
]
},
{
title: "Products", items: [
{ label: "Bread", href: "#products" },
{ label: "Pastries", href: "#products" },
{ label: "Cakes", href: "#products" }
]
},
{
title: "Legal", items: [
{ label: "Privacy Policy", href: "/" },
{ label: "Terms of Service", href: "/" }
]
}
];
export default function Home() {
return (
<ThemeProvider
defaultButtonVariant="icon-arrow"
defaultTextAnimation="background-highlight"
borderRadius="soft"
contentWidth="mediumLarge"
sizing="largeSmallSizeLargeTitles"
background="none"
cardStyle="gradient-bordered"
primaryButtonStyle="primary-glow"
defaultButtonVariant="text-stagger"
defaultTextAnimation="entrance-slide"
borderRadius="rounded"
contentWidth="medium"
sizing="medium"
background="circleGradient"
cardStyle="glass-elevated"
primaryButtonStyle="gradient"
secondaryButtonStyle="glass"
headingFontWeight="semibold"
headingFontWeight="normal"
>
<div id="nav" data-section="nav">
<NavbarLayoutFloatingOverlay
brandName="Al Rayah"
navItems={[
{ name: "Home", id: "hero" },
{ name: "About", id: "about" },
{ name: "Products", id: "products" },
{ name: "Reviews", id: "testimonials" },
{ name: "Contact", id: "contact" },
]}
button={{ text: "Call Now", href: "tel:0654650200" }}
buttonClassName="bg-primary-cta text-primary-cta-text hover:opacity-90"
/>
<NavbarLayoutFloatingOverlay navItems={navItems} />
</div>
<div id="hero" data-section="hero">
<HeroSplitTestimonial
title="Fresh Bread & Pastries in Sharjah — Open 24 Hours"
description="Al Rayah Bakeries serves the finest fresh-baked bread, pastries, and cakes to the Sharjah community. Quality, freshness, and affordable prices every single day."
tag="⭐ 4.3 Rating | 270+ Reviews"
tagAnimation="slide-up"
title="Al Rayah Bakeries"
description="Fresh-baked bread, pastries, and cakes delivered to your door in Sharjah"
background={{ variant: "plain" }}
imageSrc="http://img.b2bpic.net/free-photo/baker-holding-fresh-bread-hands_169016-1421.jpg"
imageAlt="Fresh baked bread and pastries at Al Rayah Bakeries"
mediaAnimation="opacity"
imagePosition="right"
testimonials={[
{
name: "Ahmed Al Mansouri", handle: "Local Customer", testimonial: "Fresh bread and great service. Always open when I need it!", rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/cheerful-multiracial-business-team_1262-21007.jpg?_wi=1", imageAlt: "Ahmed Al Mansouri"
},
{
name: "Fatima Al Kaabi", handle: "Regular Customer", testimonial: "Very affordable bakery in Sharjah. The pastries are delicious!", rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/signs-symbols-gestures-concept-cheerful-young-mixed-race-lady-with-long-straight-hair-smiling-broadly-showing-thumbs-up-gesture-as-sign-approval-respect-liking_344912-1033.jpg?_wi=1", imageAlt: "Fatima Al Kaabi"
},
{
name: "Mohammed Al Mazrouei", handle: "Business Owner", testimonial: "Best quality bread for my restaurant. Reliable and fast delivery!", rating: 5,
imageSrc: "http://img.b2bpic.net/free-photo/smiling-businessman-standing-airport_107420-85035.jpg?_wi=1", imageAlt: "Mohammed Al Mazrouei"
},
]}
testimonialRotationInterval={5000}
buttons={[
{ text: "Call Now", href: "tel:0654650200" },
{ text: "Order Pickup", href: "#products" },
]}
buttonAnimation="slide-up"
testimonials={heroTestimonials}
useInvertedBackground={false}
mediaAnimation="slide-up"
imageSrc="/placeholders/placeholder1.webp"
imageAlt="Al Rayah Bakeries"
/>
</div>
<div id="about" data-section="about">
<MetricSplitMediaAbout
tag="About Us"
tagAnimation="slide-up"
title="Trusted by Sharjah Families for Fresh Baked Goodness"
description="Al Rayah Bakeries has been serving the Sharjah community with fresh bread, pastries, and baked goods. We focus on quality ingredients, traditional baking methods, and affordable prices for everyone. Open 24 hours to serve you whenever you need us."
metrics={[
{ value: "24/7", title: "Always Open" },
{ value: "Fresh", title: "Daily Baked" },
]}
title="About Al Rayah Bakeries"
description="Serving Sharjah with the finest baked goods since our founding"
metrics={metrics}
useInvertedBackground={false}
imageSrc="http://img.b2bpic.net/free-photo/high-angle-arepas-basket_23-2148742414.jpg?_wi=1"
imageAlt="Fresh Arabic bread from Al Rayah Bakeries"
mediaAnimation="opacity"
metricsAnimation="slide-up"
mediaAnimation="slide-up"
imageSrc="/placeholders/placeholder1.webp"
imageAlt="About us"
/>
</div>
<div id="products" data-section="products">
<ProductCardFour
title="Featured Products"
description="Explore our selection of fresh-baked breads, pastries, cakes, and sweets made daily"
tag="Menu Items"
tagAnimation="slide-up"
textboxLayout="default"
gridVariant="uniform-all-items-equal"
products={products}
title="Our Products"
description="Explore our delicious selection of fresh baked goods"
gridVariant="four-items-2x2-equal-grid"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
products={[
{
id: "1", name: "Fresh Arabic Bread", price: "AED 1-3", variant: "Daily Fresh Baked", imageSrc: "http://img.b2bpic.net/free-photo/high-angle-arepas-basket_23-2148742414.jpg?_wi=2", imageAlt: "Fresh Arabic bread"
},
{
id: "2", name: "Assorted Pastries", price: "AED 5-15", variant: "Sweet & Savory", imageSrc: "http://img.b2bpic.net/free-photo/front-view-chocolate-chips-cake-with-raisins-fruits-white-background-pie-cookie-biscuit-sweet-cake_140725-115586.jpg", imageAlt: "Assorted pastries selection"
},
{
id: "3", name: "Custom Cakes", price: "AED 20-50", variant: "Celebrations & Events", imageSrc: "http://img.b2bpic.net/free-photo/woman-looking-turkish-sweets-shop_107420-94726.jpg", imageAlt: "Custom decorated cakes"
},
{
id: "4", name: "Traditional Sweets", price: "AED 5-25", variant: "Arabic Specialties", imageSrc: "http://img.b2bpic.net/free-photo/pasta-with-yellow_1203-1348.jpg", imageAlt: "Traditional arabic sweets"
},
]}
buttons={[{ text: "Order Now", href: "tel:0654650200" }]}
/>
</div>
<div id="features" data-section="features">
<FeatureBorderGlow
title="Why Customers Choose Al Rayah"
description="Quality, freshness, and service that keeps our community coming back"
textboxLayout="default"
features={features}
title="Why Choose Us"
description="Quality, freshness, and service that exceeds expectations"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
features={[
{
icon: Sparkles,
title: "Freshly Baked Daily", description: "All products baked fresh every morning and throughout the day"
},
{
icon: DollarSign,
title: "Affordable Prices", description: "AED 1-50 per person — quality bakery goods for every budget"
},
{
icon: Clock,
title: "Open 24 Hours", description: "Always available whenever you need fresh bread and pastries"
},
{
icon: Zap,
title: "Quick Pickup", description: "Fast service and convenient pickup for your busy schedule"
},
{
icon: Truck,
title: "Fast Local Delivery", description: "Quick delivery service throughout Sharjah"
},
{
icon: Heart,
title: "Trusted Quality", description: "4.3★ rating from 270+ satisfied customers"
},
]}
/>
</div>
<div id="testimonials" data-section="testimonials">
<TestimonialCardTwo
testimonials={testimonials}
title="What Our Customers Say"
description="Real feedback from families and businesses who trust Al Rayah Bakeries"
tag="Customer Reviews"
tagAnimation="slide-up"
textboxLayout="default"
description="Real feedback from our valued customers"
animationType="slide-up"
textboxLayout="default"
useInvertedBackground={false}
testimonials={[
{
id: "1", name: "Sarah Johnson", role: "Family Customer", testimonial: "Fresh bread and great service. Always open when I need it! My family loves coming here.", imageSrc: "http://img.b2bpic.net/free-photo/cheerful-multiracial-business-team_1262-21007.jpg?_wi=2", imageAlt: "Sarah Johnson"
},
{
id: "2", name: "Michael Chen", role: "Restaurant Owner", testimonial: "Best quality bread for my restaurant. Reliable and fast delivery. Highly recommended!", imageSrc: "http://img.b2bpic.net/free-photo/signs-symbols-gestures-concept-cheerful-young-mixed-race-lady-with-long-straight-hair-smiling-broadly-showing-thumbs-up-gesture-as-sign-approval-respect-liking_344912-1033.jpg?_wi=2", imageAlt: "Michael Chen"
},
{
id: "3", name: "Emily Rodriguez", role: "Office Manager", testimonial: "Very affordable bakery in Sharjah. The pastries are delicious and perfect for office meetings.", imageSrc: "http://img.b2bpic.net/free-photo/smiling-businessman-standing-airport_107420-85035.jpg?_wi=2", imageAlt: "Emily Rodriguez"
},
{
id: "4", name: "David Kim", role: "Late-Night Customer", testimonial: "Love that you're open 24 hours! Perfect for my night shift. Fresh bread always available.", imageSrc: "http://img.b2bpic.net/free-photo/man-looking-shop-mirror-night-out_23-2149143996.jpg", imageAlt: "David Kim"
},
]}
/>
</div>
<div id="contact" data-section="contact">
<ContactSplit
tag="Ready to Order?"
title="Craving Fresh Bread & Pastries?"
description="Contact Al Rayah Bakeries today. We're open 24 hours with quick pickup and delivery options throughout Sharjah."
background={{ variant: "radial-gradient" }}
tag="Get In Touch"
title="Order Your Fresh Baked Goods Today"
description="Contact us for bulk orders, special requests, or delivery information"
background={{ variant: "plain" }}
useInvertedBackground={false}
imageSrc="http://img.b2bpic.net/free-photo/glass-jar-milk-dried-dates-basket-gogals-marble-table_114579-29995.jpg"
imageAlt="Fresh bakery breakfast items"
mediaAnimation="opacity"
mediaPosition="right"
mediaAnimation="slide-up"
inputPlaceholder="Enter your email"
buttonText="Get Updates"
termsText="We respect your privacy. Unsubscribe at any time."
buttonText="Sign Up"
/>
</div>
<div id="footer" data-section="footer">
<FooterSimple
columns={[
{
title: "Contact", items: [
{ label: "Call Now", href: "tel:0654650200" },
{ label: "WhatsApp", href: "https://wa.me/971654650200" },
{
label: "Get Directions", href: "https://maps.google.com/?q=Al+Sharq+St+Al+Qulai'aah+Sharjah"
},
],
},
{
title: "Services", items: [
{ label: "Pickup Orders", href: "#products" },
{ label: "Delivery", href: "tel:0654650200" },
{ label: "Custom Cakes", href: "#products" },
{ label: "Catering", href: "tel:0654650200" },
],
},
{
title: "Information", items: [
{ label: "About Us", href: "#about" },
{ label: "Hours: 24/7", href: "#" },
{ label: "Reviews", href: "#testimonials" },
],
},
{
title: "Location", items: [
{
label: "Al Sharq St", href: "https://maps.google.com/?q=Al+Sharq+St+Al+Qulai'aah+Sharjah"
},
{
label: "Al Qulai'aah", href: "https://maps.google.com/?q=Al+Sharq+St+Al+Qulai'aah+Sharjah"
},
{
label: "Sharjah, UAE", href: "https://maps.google.com/?q=Al+Sharq+St+Al+Qulai'aah+Sharjah"
},
],
},
]}
bottomLeftText="© 2025 Al Rayah Bakeries. All rights reserved."
bottomRightText="Open 24 Hours | Fresh Daily"
columns={footerColumns}
bottomLeftText="© 2025 Al Rayah Bakeries"
bottomRightText="All rights reserved"
/>
</div>
</ThemeProvider>

View File

@@ -1,187 +1,47 @@
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";
import { useEffect, useRef, useState } from "react";
import { useMediaQuery } from "@/hooks/useMediaQuery";
gsap.registerPlugin(ScrollTrigger);
interface UseCardAnimationProps {
animationType: CardAnimationType | "depth-3d";
itemCount: number;
isGrid?: boolean;
supports3DAnimation?: boolean;
gridVariant?: GridVariant;
useIndividualTriggers?: boolean;
interface CardAnimationConfig {
duration?: number;
delay?: number;
stagger?: number;
}
export const useCardAnimation = ({
animationType,
itemCount,
isGrid = true,
supports3DAnimation = false,
gridVariant,
useIndividualTriggers = false
}: UseCardAnimationProps) => {
const itemRefs = useRef<(HTMLElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement | null>(null);
const perspectiveRef = useRef<HTMLDivElement | null>(null);
const bottomContentRef = useRef<HTMLDivElement | null>(null);
export const useCardAnimation = (
config?: CardAnimationConfig
) => {
const { duration = 0.6, delay = 0, stagger = 0.1 } = config || {};
const containerRef = useRef<HTMLDivElement>(null);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const isMobile = useMediaQuery("(max-width: 768px)");
const [isAnimating, setIsAnimating] = useState(false);
// Enable 3D effect only when explicitly supported and conditions are met
const { isMobile } = useDepth3DAnimation({
itemRefs,
useEffect(() => {
if (!containerRef.current) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isAnimating) {
setIsAnimating(true);
}
},
{ threshold: 0.1 }
);
observer.observe(containerRef.current);
return () => {
observer.disconnect();
};
}, [isAnimating]);
return {
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);
}
if (effectiveAnimationType === "opacity") {
if (useIndividualTriggers) {
items.forEach((item) => {
gsap.fromTo(
item,
{ opacity: 0 },
{
opacity: 1,
duration: 1.25,
ease: "sine",
scrollTrigger: {
trigger: item,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
} 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]);
return { itemRefs, containerRef, perspectiveRef, bottomContentRef };
itemRefs,
isMobile,
isAnimating,
duration,
delay,
stagger,
};
};

View File

@@ -17,3 +17,5 @@ export const TimelineBase: React.FC<TimelineBaseProps> = ({
</div>
);
};
export default TimelineBase;

View File

@@ -1,156 +1,75 @@
"use client";
import React, { useState } from "react";
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";
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;
interface CatalogProduct {
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt: string;
rating: number;
reviewCount: string;
category: string;
onProductClick: () => void;
}
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 Product {
id: string;
name: string;
price: string;
category: string;
}
const handleProductClick = useCallback((productId: string) => {
router.push(`/shop/${productId}`);
}, [router]);
interface ProductCatalogProps {
products?: CatalogProduct[];
categories?: string[];
onCategoryChange?: (category: string) => void;
className?: string;
}
const products: CatalogProduct[] = useMemo(() => {
if (productsProp && productsProp.length > 0) {
return productsProp;
}
export const ProductCatalog: React.FC<ProductCatalogProps> = ({
products = [],
categories = [],
onCategoryChange,
className,
}) => {
const [selectedCategory, setSelectedCategory] = useState<string>("all");
const [filteredProducts, setFilteredProducts] = useState<CatalogProduct[]>(products);
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>
);
const handleCategoryChange = (category: string) => {
setSelectedCategory(category);
if (onCategoryChange) {
onCategoryChange(category);
}
if (category === "all") {
setFilteredProducts(products);
} else {
setFilteredProducts(products.filter((p) => p.category === category));
}
};
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}>
<div className="category-filter">
<button onClick={() => handleCategoryChange("all")}>All</button>
{categories.map((cat) => (
<button key={cat} onClick={() => handleCategoryChange(cat)}>
{cat}
</button>
))}
</div>
<div className="products-grid">
{filteredProducts.map((product) => (
<div key={product.id} className="product-card">
<img src={product.imageSrc} alt={product.imageAlt} />
<h3>{product.name}</h3>
<p>{product.price}</p>
<div className="rating">
{product.rating} stars ({product.reviewCount} reviews)
</div>
<button onClick={product.onProductClick}>View Details</button>
</div>
))}
</div>
</div>
);
};
ProductCatalog.displayName = "ProductCatalog";
export default memo(ProductCatalog);

View File

@@ -7,9 +7,30 @@ interface ContactSplitProps {
background?: { variant: string };
useInvertedBackground?: boolean;
imageSrc?: string;
videoSrc?: string;
imageAlt?: string;
videoAriaLabel?: string;
mediaAnimation?: string;
mediaPosition?: string;
inputPlaceholder?: string;
buttonText?: string;
termsText?: string;
onSubmit?: (email: string) => void;
className?: string;
containerClassName?: string;
contentClassName?: string;
contactFormClassName?: string;
tagClassName?: string;
titleClassName?: string;
descriptionClassName?: string;
formWrapperClassName?: string;
formClassName?: string;
inputClassName?: string;
buttonClassName?: string;
buttonTextClassName?: string;
termsClassName?: string;
mediaWrapperClassName?: string;
mediaClassName?: string;
}
export const ContactSplit: React.FC<ContactSplitProps> = ({
@@ -19,36 +40,69 @@ export const ContactSplit: React.FC<ContactSplitProps> = ({
background,
useInvertedBackground = false,
imageSrc,
inputPlaceholder = "Enter your email", buttonText = "Sign Up", className,
videoSrc,
imageAlt = "", videoAriaLabel = "Contact section video", mediaAnimation = "slide-up", mediaPosition = "right", inputPlaceholder = "Enter your email", buttonText = "Sign Up", termsText = "By clicking Sign Up you're confirming that you agree with our Terms and Conditions.", onSubmit,
className,
containerClassName,
contentClassName,
contactFormClassName,
tagClassName,
titleClassName,
descriptionClassName,
formWrapperClassName,
formClassName,
inputClassName,
buttonClassName,
buttonTextClassName,
termsClassName,
mediaWrapperClassName,
mediaClassName,
}) => {
const [email, setEmail] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (onSubmit) {
onSubmit(email);
}
setEmail("");
};
return (
<div className={className}>
<div className="contact-split-container">
<div className="contact-content">
<span className="tag">{tag}</span>
<h2>{title}</h2>
<p>{description}</p>
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder={inputPlaceholder}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit">{buttonText}</button>
</form>
<div className={containerClassName}>
<div className={contentClassName}>
<span className={tagClassName}>{tag}</span>
<h2 className={titleClassName}>{title}</h2>
<p className={descriptionClassName}>{description}</p>
<div className={contactFormClassName}>
<form onSubmit={handleSubmit} className={formClassName}>
<input
type="email"
placeholder={inputPlaceholder}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className={inputClassName}
/>
<button type="submit" className={buttonClassName}>
<span className={buttonTextClassName}>{buttonText}</span>
</button>
</form>
{termsText && <p className={termsClassName}>{termsText}</p>}
</div>
</div>
{imageSrc && (
<div className="contact-media">
<img src={imageSrc} alt="Contact visual" />
{(imageSrc || videoSrc) && (
<div className={mediaWrapperClassName}>
{imageSrc && <img src={imageSrc} alt={imageAlt} className={mediaClassName} />}
{videoSrc && (
<video
src={videoSrc}
aria-label={videoAriaLabel}
controls
className={mediaClassName}
/>
)}
</div>
)}
</div>

View File

@@ -1,238 +1,85 @@
"use client";
import React from "react";
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";
type ProductCardFourGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
interface ProductCard {
id: string;
name: string;
price: string;
variant: string;
};
imageSrc: string;
imageAlt?: string;
onProductClick?: () => void;
isFavorited?: boolean;
onFavorite?: () => void;
}
interface ProductCardFourProps {
products?: ProductCard[];
carouselMode?: "auto" | "buttons";
gridVariant: ProductCardFourGridVariant;
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;
title?: string;
description?: string;
gridVariant?: string;
animationType?: string;
textboxLayout?: string;
useInvertedBackground?: boolean;
className?: string;
containerClassName?: string;
cardClassName?: string;
imageClassName?: string;
textBoxTitleClassName?: string;
textBoxTitleImageWrapperClassName?: string;
textBoxTitleImageClassName?: string;
textBoxDescriptionClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardVariantClassName?: 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;
cardVariantClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardVariantClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
export const ProductCardFour: React.FC<ProductCardFourProps> = ({
products = [],
title = "Products", description = "Our collection", gridVariant = "uniform-all-items-equal", animationType = "none", textboxLayout = "default", useInvertedBackground = false,
className,
containerClassName,
cardClassName,
imageClassName,
cardNameClassName,
cardPriceClassName,
cardVariantClassName,
gridClassName,
textBoxClassName,
}) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-0 flex-1 min-w-0">
<h3 className={cls("text-base font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background/60" : "text-foreground/60", cardVariantClassName)}>
{product.variant}
</p>
</div>
<p className={cls("text-base font-medium leading-[1.3] flex-shrink-0", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
<div className={className}>
<div className={containerClassName}>
<div className={textBoxClassName}>
<h2>{title}</h2>
<p>{description}</p>
</div>
<div className={gridClassName}>
{products.map((product) => (
<div key={product.id} className={cardClassName}>
<div className="product-image-wrapper">
<img
src={product.imageSrc}
alt={product.imageAlt || product.name}
className={imageClassName}
/>
<button
className="favorite-btn"
onClick={product.onFavorite}
aria-pressed={product.isFavorited}
>
{product.isFavorited ? "♥" : "♡"}
</button>
</div>
<h3 className={cardNameClassName}>{product.name}</h3>
<p className={cardVariantClassName}>{product.variant}</p>
<p className={cardPriceClassName}>{product.price}</p>
<button onClick={product.onProductClick} className="view-btn">
View Details
</button>
</div>
))}
</div>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
const ProductCardFour = ({
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 = "",
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);
const handleProductClick = useCallback((product: ProductCard) => {
if (isFromApi) {
router.push(`/shop/${product.id}`);
} else {
product.onProductClick?.();
}
}, [isFromApi, router]);
if (isLoading && !productsProp) {
return (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
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) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
cardVariantClassName={cardVariantClassName}
actionButtonClassName={actionButtonClassName}
/>
))}
</CardStack>
</div>
);
};
ProductCardFour.displayName = "ProductCardFour";
export default ProductCardFour;

View File

@@ -1,226 +1,57 @@
"use client";
import React from "react";
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";
type ProductCardOneGridVariant = Exclude<GridVariant, "timeline">;
type ProductCard = Product;
interface Product {
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
onProductClick?: () => void;
isFavorited?: boolean;
onFavorite?: () => 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[];
title?: string;
description?: string;
className?: string;
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
imageClassName={imageClassName}
/>
<div className="relative z-1 flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<h3 className={cls("text-base font-medium truncate leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
</div>
<button
className="relative cursor-pointer primary-button h-10 w-auto aspect-square rounded-theme flex items-center justify-center flex-shrink-0"
aria-label={`View ${product.name} details`}
type="button"
>
<ArrowUpRight className="h-4/10 text-primary-cta-text transition-transform duration-300 group-hover:rotate-45" strokeWidth={1.5} />
</button>
export const ProductCardOne: React.FC<ProductCardOneProps> = ({
products = [],
title = "Products", description = "Our collection", className,
}) => {
return (
<div className={className}>
<h2>{title}</h2>
<p>{description}</p>
<div className="products-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<div className="product-image">
<img
src={product.imageSrc}
alt={product.imageAlt || product.name}
/>
<button
className="favorite-btn"
onClick={product.onFavorite}
aria-pressed={product.isFavorited}
>
{product.isFavorited ? "♥" : "♡"}
</button>
</div>
</article>
);
});
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 (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
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) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
/>
))}
</CardStack>
);
<h3>{product.name}</h3>
<p className="price">{product.price}</p>
<button onClick={product.onProductClick} className="view-btn">
View Details
</button>
</div>
))}
</div>
</div>
);
};
ProductCardOne.displayName = "ProductCardOne";
export default ProductCardOne;

View File

@@ -1,283 +1,57 @@
"use client";
import React from "react";
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";
type ProductCardThreeGridVariant = Exclude<GridVariant, "timeline" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row">;
type ProductCard = Product & {
onQuantityChange?: (quantity: number) => void;
initialQuantity?: number;
priceButtonProps?: Partial<ButtonPropsForVariant<CTAButtonVariant>>;
};
interface ProductCard {
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
onProductClick?: () => void;
isFavorited?: boolean;
onFavorite?: () => 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[];
title?: string;
description?: string;
className?: string;
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
isFromApi: boolean;
onBuyClick?: (productId: string, quantity: number) => void;
cardClassName?: string;
imageClassName?: string;
cardNameClassName?: string;
quantityControlsClassName?: string;
}
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]);
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={handleClick}
role="article"
aria-label={`${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || product.name}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
imageClassName={imageClassName}
/>
<div className="relative z-1 flex flex-col gap-3">
<h3 className={cls("text-xl font-medium leading-[1.15] truncate", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className="flex items-center justify-between gap-4">
<div className={cls("flex items-center gap-2", quantityControlsClassName)}>
<QuantityButton
onClick={handleDecrement}
ariaLabel="Decrease quantity"
Icon={Minus}
/>
<span className={cls("text-base font-medium min-w-[2ch] text-center leading-[1]", shouldUseLightText ? "text-background" : "text-foreground")}>
{quantity}
</span>
<QuantityButton
onClick={handleIncrement}
ariaLabel="Increase quantity"
Icon={Plus}
/>
</div>
<Button
{...getButtonProps(
{
text: product.price,
props: product.priceButtonProps,
},
0,
theme.defaultButtonVariant
)}
/>
</div>
export const ProductCardThree: React.FC<ProductCardThreeProps> = ({
products = [],
title = "Products", description = "Our collection", className,
}) => {
return (
<div className={className}>
<h2>{title}</h2>
<p>{description}</p>
<div className="products-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<div className="product-image">
<img
src={product.imageSrc}
alt={product.imageAlt || product.name}
/>
<button
className="favorite-btn"
onClick={product.onFavorite}
aria-pressed={product.isFavorited}
>
{product.isFavorited ? "♥" : "♡"}
</button>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
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 (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
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) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
isFromApi={isFromApi}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardNameClassName={cardNameClassName}
quantityControlsClassName={quantityControlsClassName}
/>
))}
</CardStack>
);
<h3>{product.name}</h3>
<p className="price">{product.price}</p>
<button onClick={product.onProductClick} className="view-btn">
View Details
</button>
</div>
))}
</div>
</div>
);
};
ProductCardThree.displayName = "ProductCardThree";
export default ProductCardThree;

View File

@@ -1,267 +1,57 @@
"use client";
import React from "react";
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";
type ProductCardTwoGridVariant = Exclude<GridVariant, "timeline" | "one-large-right-three-stacked-left" | "items-top-row-full-width-bottom" | "full-width-top-items-bottom-row" | "one-large-left-three-stacked-right">;
type ProductCard = Product & {
brand: string;
rating: number;
reviewCount: string;
};
interface ProductCard {
id: string;
name: string;
price: string;
imageSrc: string;
imageAlt?: string;
onProductClick?: () => void;
isFavorited?: boolean;
onFavorite?: () => 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[];
title?: string;
description?: string;
className?: string;
}
interface ProductCardItemProps {
product: ProductCard;
shouldUseLightText: boolean;
cardClassName?: string;
imageClassName?: string;
cardBrandClassName?: string;
cardNameClassName?: string;
cardPriceClassName?: string;
cardRatingClassName?: string;
actionButtonClassName?: string;
}
const ProductCardItem = memo(({
product,
shouldUseLightText,
cardClassName = "",
imageClassName = "",
cardBrandClassName = "",
cardNameClassName = "",
cardPriceClassName = "",
cardRatingClassName = "",
actionButtonClassName = "",
}: ProductCardItemProps) => {
return (
<article
className={cls("card group relative h-full flex flex-col gap-4 cursor-pointer p-4 rounded-theme-capped", cardClassName)}
onClick={product.onProductClick}
role="article"
aria-label={`${product.brand} ${product.name} - ${product.price}`}
>
<ProductImage
imageSrc={product.imageSrc}
imageAlt={product.imageAlt || `${product.brand} ${product.name}`}
isFavorited={product.isFavorited}
onFavoriteToggle={product.onFavorite}
showActionButton={true}
actionButtonAriaLabel={`View ${product.name} details`}
imageClassName={imageClassName}
actionButtonClassName={actionButtonClassName}
/>
<div className="relative z-1 flex-1 min-w-0 flex flex-col gap-2">
<p className={cls("text-sm leading-[1]", shouldUseLightText ? "text-background" : "text-foreground", cardBrandClassName)}>
{product.brand}
</p>
<div className="flex flex-col gap-1" >
<h3 className={cls("text-xl font-medium truncate leading-[1.15]", shouldUseLightText ? "text-background" : "text-foreground", cardNameClassName)}>
{product.name}
</h3>
<div className={cls("flex items-center gap-2", cardRatingClassName)}>
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={cls(
"h-4 w-auto",
i < Math.floor(product.rating)
? "text-accent fill-accent"
: "text-accent opacity-20"
)}
strokeWidth={1.5}
/>
))}
</div>
<span className={cls("text-sm leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground")}>
({product.reviewCount})
</span>
</div>
</div>
<p className={cls("text-2xl font-medium leading-[1.3]", shouldUseLightText ? "text-background" : "text-foreground", cardPriceClassName)}>
{product.price}
</p>
export const ProductCardTwo: React.FC<ProductCardTwoProps> = ({
products = [],
title = "Products", description = "Our collection", className,
}) => {
return (
<div className={className}>
<h2>{title}</h2>
<p>{description}</p>
<div className="products-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<div className="product-image">
<img
src={product.imageSrc}
alt={product.imageAlt || product.name}
/>
<button
className="favorite-btn"
onClick={product.onFavorite}
aria-pressed={product.isFavorited}
>
{product.isFavorited ? "♥" : ""}
</button>
</div>
</article>
);
});
ProductCardItem.displayName = "ProductCardItem";
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 (
<div className="w-content-width mx-auto py-20 text-center">
<p className="text-foreground">Loading products...</p>
</div>
);
}
if (!products || products.length === 0) {
return null;
}
return (
<CardStack
useInvertedBackground={useInvertedBackground}
mode={carouselMode}
gridVariant={gridVariant}
uniformGridCustomHeightClasses={uniformGridCustomHeightClasses}
gridRowsClassName={customGridRows}
animationType={animationType}
title={title}
titleSegments={titleSegments}
description={description}
tag={tag}
tagIcon={tagIcon}
tagAnimation={tagAnimation}
buttons={buttons}
buttonAnimation={buttonAnimation}
textboxLayout={textboxLayout}
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) => (
<ProductCardItem
key={`${product.id}-${index}`}
product={{ ...product, onProductClick: () => handleProductClick(product) }}
shouldUseLightText={shouldUseLightText}
cardClassName={cardClassName}
imageClassName={imageClassName}
cardBrandClassName={cardBrandClassName}
cardNameClassName={cardNameClassName}
cardPriceClassName={cardPriceClassName}
cardRatingClassName={cardRatingClassName}
actionButtonClassName={actionButtonClassName}
/>
))}
</CardStack>
);
<h3>{product.name}</h3>
<p className="price">{product.price}</p>
<button onClick={product.onProductClick} className="view-btn">
View Details
</button>
</div>
))}
</div>
</div>
);
};
ProductCardTwo.displayName = "ProductCardTwo";
export default ProductCardTwo;

View File

@@ -0,0 +1,18 @@
import { useEffect, useState } from "react";
export const useMediaQuery = (query: string): boolean => {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
if (media.matches !== matches) {
setMatches(media.matches);
}
const listener = () => setMatches(media.matches);
media.addListener(listener);
return () => media.removeListener(listener);
}, [matches, query]);
return matches;
};

View File

@@ -1,45 +1,39 @@
"use client";
import { useState, useEffect } from "react";
import { fetchProductById } from "@/lib/api/product";
import { useEffect, useState } from "react";
import { Product, fetchProduct } from "@/lib/api/product";
export function useProduct(productId: string) {
const [product, setProduct] = useState<Product | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(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 ProductDetail {
id: string;
name: string;
price: number;
description: string;
}
export const useProduct = (productId: string) => {
const [product, setProduct] = useState<ProductDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadProduct = async () => {
try {
setLoading(true);
const result = await fetchProductById(productId);
if (result) {
setProduct(result as ProductDetail);
} else {
setError("Product not found");
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch product");
} finally {
setLoading(false);
}
};
if (productId) {
loadProduct();
}
}, [productId]);
return { product, loading, error };
};