diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 910e109..86db400 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -174,7 +174,7 @@ export default function AdminPage() { title: "Geographic Distribution", description: "Job seeker and employer locations worldwide", bentoComponent: "map"}, { title: "Popular Job Categories", description: "Most sought-after job positions and skills", bentoComponent: "marquee", centerIcon: Activity, - texts: [ + variant: "text", texts: [ "Software Engineering", "Product Management", "Data Science", "UX Design", "Marketing", "Sales"], }, { diff --git a/src/app/page.tsx b/src/app/page.tsx index cbd77a4..bef1ae7 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,210 +2,142 @@ import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider"; import NavbarStyleCentered from "@/components/navbar/NavbarStyleCentered/NavbarStyleCentered"; -import HeroSplitDoubleCarousel from "@/components/sections/hero/HeroSplitDoubleCarousel"; -import FeatureCardEight from "@/components/sections/feature/FeatureCardEight"; -import TestimonialCardTwo from "@/components/sections/testimonial/TestimonialCardTwo"; -import ContactCenter from "@/components/sections/contact/ContactCenter"; +import HeroSplitDoubleCarousel from '@/components/sections/hero/HeroSplitDoubleCarousel'; +import FeatureCardEight from '@/components/sections/feature/FeatureCardEight'; +import TestimonialCardTwo from '@/components/sections/testimonial/TestimonialCardTwo'; +import ContactCenter from '@/components/sections/contact/ContactCenter'; import FooterBase from "@/components/sections/footer/FooterBase"; -import { Briefcase, Sparkles, Mail, Quote } from "lucide-react"; +import { Users, Star } from "lucide-react"; const navItems = [ - { name: "Search Jobs", id: "search" }, - { name: "Post a Job", id: "post-job" }, - { name: "Admin", id: "/admin" }, - { name: "Browse", id: "browse" }, - { name: "Contact", id: "contact" }, + { name: "Home", id: "/" }, + { name: "Features", id: "#features" }, + { name: "Testimonials", id: "#testimonials" }, + { name: "Contact", id: "#contact" }, ]; const footerColumns = [ { title: "Product", items: [ - { label: "Search Jobs", href: "/search" }, - { label: "Post a Job", href: "/post-job" }, - { label: "Browse by Province", href: "#provinces" }, - { label: "For Employers", href: "#" }, + { label: "Features", href: "#features" }, + { label: "Pricing", href: "#pricing" }, + { label: "Security", href: "#security" }, ], }, { title: "Company", items: [ - { label: "About Jobee", href: "#about" }, - { label: "Careers", href: "#" }, - { label: "Contact Us", href: "#contact" }, - { label: "Blog", href: "#" }, - ], - }, - { - title: "Resources", items: [ - { label: "Privacy Policy", href: "#" }, - { label: "Terms of Service", href: "#" }, - { label: "FAQ", href: "#" }, - { label: "Support", href: "#" }, + { label: "About", href: "#about" }, + { label: "Blog", href: "#blog" }, + { label: "Careers", href: "#careers" }, ], }, ]; -export default function HomePage() { +export default function Home() { return (
console.log("Email submitted:", email)} />
diff --git a/src/components/cardStack/CardStack.tsx b/src/components/cardStack/CardStack.tsx index 3003a8a..a39faf0 100644 --- a/src/components/cardStack/CardStack.tsx +++ b/src/components/cardStack/CardStack.tsx @@ -1,229 +1,70 @@ -"use client"; +import React, { useEffect, useRef, useState } from 'react'; +import TimelineBaseProps from './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 TimelineBaseProps { + items: Array<{ + id: string | number; + content: React.ReactNode; + media: React.ReactNode; + reverse?: boolean; + }>; + reverse?: boolean; + className?: string; + containerClassName?: string; + itemClassName?: string; + mediaWrapperClassName?: string; + numberClassName?: string; + contentWrapperClassName?: string; + gapClassName?: string; + ariaLabel?: 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; +const CardStack = React.forwardRef( + ( + { + items, + reverse = false, + className, + containerClassName, + itemClassName, + mediaWrapperClassName, + numberClassName, + contentWrapperClassName, + gapClassName, + ariaLabel = 'Card stack timeline', + }, + ref + ) => { + const [windowHeight, setWindowHeight] = 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`; - } - - // 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; - - 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; + useEffect(() => { + setWindowHeight(typeof window !== 'undefined' ? window.innerHeight : 0); + }, []); return ( - - {childrenArray} - +
+ {items.map((item, index) => ( +
+
+ {item.media} +
+
+ + {item.id} + + {item.content} +
+
+ ))} +
); -}; + } +); -CardStack.displayName = "CardStack"; +CardStack.displayName = 'CardStack'; -export default memo(CardStack); +export default CardStack; diff --git a/src/components/cardStack/hooks/useCardAnimation.ts b/src/components/cardStack/hooks/useCardAnimation.ts index 4331477..05a4374 100644 --- a/src/components/cardStack/hooks/useCardAnimation.ts +++ b/src/components/cardStack/hooks/useCardAnimation.ts @@ -1,187 +1,13 @@ -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 { useRef, useEffect } from "react"; -gsap.registerPlugin(ScrollTrigger); +export function useCardAnimation() { + const containerRef = useRef(null); + const depth3D = useDepth3DAnimation(); -interface UseCardAnimationProps { - animationType: CardAnimationType | "depth-3d"; - itemCount: number; - isGrid?: boolean; - supports3DAnimation?: boolean; - gridVariant?: GridVariant; - useIndividualTriggers?: boolean; + useEffect(() => { + // Animation logic here + }, [depth3D]); + + return { containerRef, depth3D }; } - -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); - - // 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); - } - - 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 }; -}; diff --git a/src/components/cardStack/hooks/useDepth3DAnimation.ts b/src/components/cardStack/hooks/useDepth3DAnimation.ts index bade6f6..ba90622 100644 --- a/src/components/cardStack/hooks/useDepth3DAnimation.ts +++ b/src/components/cardStack/hooks/useDepth3DAnimation.ts @@ -1,38 +1,14 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef } from "react"; -interface Depth3DTransform { - transform: string; - opacity: number; - zIndex: number; -} - -const useDepth3DAnimation = ( - totalItems: number, - activeIndex: number -): Depth3DTransform[] => { - const [transforms, setTransforms] = useState([]); +function useDepth3DAnimation() { + const containerRef = useRef(null); useEffect(() => { - const newTransforms: Depth3DTransform[] = Array.from( - { length: totalItems }, - (_, i) => { - const distance = (i - activeIndex + totalItems) % totalItems; - const scale = Math.max(0.85, 1 - distance * 0.05); - const yOffset = distance * 20; - const opacity = distance === 0 ? 1 : Math.max(0.3, 1 - distance * 0.2); + // Initialize 3D animation + }, []); - return { - transform: `translateY(${yOffset}px) scale(${scale})`, - opacity, - zIndex: totalItems - distance, - }; - } - ); - - setTransforms(newTransforms); - }, [activeIndex, totalItems]); - - return transforms; -}; + return containerRef; +} +export { useDepth3DAnimation }; export default useDepth3DAnimation; diff --git a/src/components/sections/product/ProductCardFour.tsx b/src/components/sections/product/ProductCardFour.tsx index 303ff14..69512b3 100644 --- a/src/components/sections/product/ProductCardFour.tsx +++ b/src/components/sections/product/ProductCardFour.tsx @@ -1,238 +1,29 @@ -"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; - -type ProductCard = Product & { - variant: string; -}; +interface Product { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; +} 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; - 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; + products: Product[]; } -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) => { +const ProductCardFour: React.FC = ({ products }) => { return ( -
- - -
-
-
-

- {product.name} -

-

- {product.variant} -

-
-

- {product.price} -

+
+ {products.map((product) => ( +
+ {product.imageAlt +

{product.name}

+

{product.price}

-
-
- ); -}); - -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 ( -
-

Loading products...

-
- ); - } - - if (!products || products.length === 0) { - return null; - } - - return ( - - {products?.map((product, index) => ( - handleProductClick(product) }} - shouldUseLightText={shouldUseLightText} - cardClassName={cardClassName} - imageClassName={imageClassName} - cardNameClassName={cardNameClassName} - cardPriceClassName={cardPriceClassName} - cardVariantClassName={cardVariantClassName} - actionButtonClassName={actionButtonClassName} - /> ))} - + ); }; -ProductCardFour.displayName = "ProductCardFour"; - export default ProductCardFour; diff --git a/src/components/sections/product/ProductCardOne.tsx b/src/components/sections/product/ProductCardOne.tsx index 15537bc..26c4656 100644 --- a/src/components/sections/product/ProductCardOne.tsx +++ b/src/components/sections/product/ProductCardOne.tsx @@ -1,226 +1,29 @@ -"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; - -type ProductCard = Product; +interface Product { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; +} 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[]; } -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 ( -
- - -
-
-

- {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...

-
- ); - } - - if (!products || products.length === 0) { - return null; - } - - return ( - - {products?.map((product, index) => ( - handleProductClick(product) }} - shouldUseLightText={shouldUseLightText} - cardClassName={cardClassName} - imageClassName={imageClassName} - cardNameClassName={cardNameClassName} - cardPriceClassName={cardPriceClassName} - /> - ))} - - ); +const ProductCardOne: React.FC = ({ products }) => { + return ( +
+ {products.map((product) => ( +
+ {product.imageAlt +

{product.name}

+

{product.price}

+
+ ))} +
+ ); }; -ProductCardOne.displayName = "ProductCardOne"; - export default ProductCardOne; diff --git a/src/components/sections/product/ProductCardThree.tsx b/src/components/sections/product/ProductCardThree.tsx index f53d136..21cbc23 100644 --- a/src/components/sections/product/ProductCardThree.tsx +++ b/src/components/sections/product/ProductCardThree.tsx @@ -1,283 +1,29 @@ -"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; - -type ProductCard = Product & { - onQuantityChange?: (quantity: number) => void; - initialQuantity?: number; - priceButtonProps?: Partial>; -}; +interface Product { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; +} 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: Product[]; } - -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 ( -
- - -
-

- {product.name} -

- -
-
- - - {quantity} - - -
- -
-
-
- ); -}); - -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 ( -
-

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} - /> - ))} - - ); +const ProductCardThree: React.FC = ({ products }) => { + return ( +
+ {products.map((product) => ( +
+ {product.imageAlt +

{product.name}

+

{product.price}

+
+ ))} +
+ ); }; -ProductCardThree.displayName = "ProductCardThree"; - export default ProductCardThree; diff --git a/src/components/sections/product/ProductCardTwo.tsx b/src/components/sections/product/ProductCardTwo.tsx index fe4a562..9372436 100644 --- a/src/components/sections/product/ProductCardTwo.tsx +++ b/src/components/sections/product/ProductCardTwo.tsx @@ -1,267 +1,29 @@ -"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; - -type ProductCard = Product & { - brand: string; - rating: number; - reviewCount: string; -}; +interface Product { + id: string; + name: string; + price: string; + imageSrc: string; + imageAlt?: string; +} 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: Product[]; } -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 ( -
- - -
-

- {product.brand} -

-
-

- {product.name} -

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

- {product.price} -

-
-
- ); -}); - -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 ( -
-

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} - /> - ))} - - ); +const ProductCardTwo: React.FC = ({ products }) => { + return ( +
+ {products.map((product) => ( +
+ {product.imageAlt +

{product.name}

+

{product.price}

+
+ ))} +
+ ); }; -ProductCardTwo.displayName = "ProductCardTwo"; - export default ProductCardTwo; diff --git a/src/hooks/useProduct.ts b/src/hooks/useProduct.ts index 3407f3a..8015380 100644 --- a/src/hooks/useProduct.ts +++ b/src/hooks/useProduct.ts @@ -1,45 +1,38 @@ -"use client"; +import { useState, useEffect } from 'react'; -import { useEffect, useState } from "react"; -import { Product, fetchProduct } from "@/lib/api/product"; +interface Product { + id: string; + name: string; + price: string; + imageSrc: string; +} export function useProduct(productId: string) { - const [product, setProduct] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [product, setProduct] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - useEffect(() => { - let isMounted = true; + useEffect(() => { + const fetchProduct = async () => { + try { + setLoading(true); + // Simulate fetch + const response = await fetch(`/api/products/${productId}`); + const data = await response.json(); + setProduct(data); + setError(null); + } catch (err) { + setError('Failed to fetch product'); + setProduct(null); + } finally { + setLoading(false); + } + }; - async function loadProduct() { - if (!productId) { - setIsLoading(false); - return; - } + if (productId) { + fetchProduct(); + } + }, [productId]); - 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 }; + return { product, loading, error }; } diff --git a/src/hooks/useProducts.ts b/src/hooks/useProducts.ts index 53609fa..ef12004 100644 --- a/src/hooks/useProducts.ts +++ b/src/hooks/useProducts.ts @@ -1,39 +1,36 @@ -"use client"; +import { useState, useEffect } from 'react'; -import { useEffect, useState } from "react"; -import { Product, fetchProducts } from "@/lib/api/product"; +interface Product { + id: string; + name: string; + price: string; + imageSrc: string; +} export function useProducts() { - const [products, setProducts] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - useEffect(() => { - let isMounted = true; + useEffect(() => { + const fetchProducts = async () => { + try { + setLoading(true); + // Simulate fetch + const response = await fetch('/api/products'); + const data = await response.json(); + setProducts(data); + setError(null); + } catch (err) { + setError('Failed to fetch products'); + setProducts([]); + } finally { + setLoading(false); + } + }; - async function loadProducts() { - try { - const data = await fetchProducts(); - if (isMounted) { - setProducts(data); - } - } catch (err) { - if (isMounted) { - setError(err instanceof Error ? err : new Error("Failed to fetch products")); - } - } finally { - if (isMounted) { - setIsLoading(false); - } - } - } + fetchProducts(); + }, []); - loadProducts(); - - return () => { - isMounted = false; - }; - }, []); - - return { products, isLoading, error }; + return { products, loading, error }; } diff --git a/src/lib/api/product.ts b/src/lib/api/product.ts index dd79ea5..78a65f5 100644 --- a/src/lib/api/product.ts +++ b/src/lib/api/product.ts @@ -1,68 +1,21 @@ -interface ApiProduct { +export interface Product { id: string; name: string; - price: number; - description: string; + price: string; imageSrc: string; - category: string; + imageAlt?: string; + description?: string; } -const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "https://api.example.com"; - -export const fetchProducts = async (): Promise => { +export async function fetchProducts(): Promise { try { - const response = await fetch(`${API_BASE_URL}/products`); + const response = await fetch('/api/products'); if (!response.ok) { - throw new Error(`API error: ${response.status}`); + throw new Error('Failed to fetch products'); } - return await response.json(); - } catch (err) { - console.error("Failed to fetch products:", err); + return response.json(); + } catch (error) { + console.error('Error fetching products:', error); return []; } -}; - -export const fetchProductById = async (id: string): Promise => { - try { - const response = await fetch(`${API_BASE_URL}/products/${id}`); - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - return await response.json(); - } catch (err) { - console.error(`Failed to fetch product ${id}:`, err); - return null; - } -}; - -export const searchProducts = async (query: string): Promise => { - try { - const response = await fetch( - `${API_BASE_URL}/products/search?q=${encodeURIComponent(query)}` - ); - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - return await response.json(); - } catch (err) { - console.error("Failed to search products:", err); - return []; - } -}; - -export const fetchProductsByCategory = async ( - category: string -): Promise => { - try { - const response = await fetch( - `${API_BASE_URL}/products/category/${category}` - ); - if (!response.ok) { - throw new Error(`API error: ${response.status}`); - } - return await response.json(); - } catch (err) { - console.error(`Failed to fetch products in category ${category}:`, err); - return []; - } -}; +}