diff --git a/src/app/page.tsx b/src/app/page.tsx index 2db2b0d..221cb39 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,22 +11,21 @@ import TestimonialCardSixteen from '@/components/sections/testimonial/Testimonia import FaqSplitText from '@/components/sections/faq/FaqSplitText'; import ContactSplitForm from '@/components/sections/contact/ContactSplitForm'; import FooterBaseCard from '@/components/sections/footer/FooterBaseCard'; -import { Shield, Brain, HardDrive, Zap, Package } from 'lucide-react'; - -const assetMap: Record = { - 'hero-dashboard': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/a-premium-discord-security-dashboard-int-1773253913572-d727d9ee.png', - 'feature-antinuke': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/visual-representation-of-antinuke-protec-1773253912702-75413cab.png', - 'feature-automod': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/ai-powered-content-moderation-system-int-1773253912974-892bfd2f.png', - 'feature-backup': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/server-backup-and-recovery-system-interf-1773253913333-eafcf1ca.png', - 'testimonial-1': 'http://img.b2bpic.net/free-photo/side-view-smiley-people-playing-videogame_23-2149349993.jpg', - 'testimonial-2': 'http://img.b2bpic.net/free-photo/afroamerican-businessman-wearing-headphones_23-2148508923.jpg', - 'testimonial-3': 'http://img.b2bpic.net/free-photo/front-view-man-posing-studio_23-2150275662.jpg', - 'testimonial-4': 'http://img.b2bpic.net/free-photo/young-chinese-woman-smiling-confident-standing-street_839833-7633.jpg', - 'contact-form-bg': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/premium-contact-interface-showing-secure-1773253914940-41b5b955.png', - 'discord-logo': 'http://img.b2bpic.net/free-vector/popular-social-media-icons_1057-4291.jpg', -}; +import { Shield, Brain, HardDrive, Zap } from 'lucide-react'; const getAssetUrl = (assetId: string): string => { + const assetMap: Record = { + 'hero-dashboard': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/a-premium-discord-security-dashboard-int-1773253913572-d727d9ee.png', + 'feature-antinuke': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/visual-representation-of-antinuke-protec-1773253912702-75413cab.png', + 'feature-automod': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/ai-powered-content-moderation-system-int-1773253912974-892bfd2f.png', + 'feature-backup': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/server-backup-and-recovery-system-interf-1773253913333-eafcf1ca.png', + 'testimonial-1': 'http://img.b2bpic.net/free-photo/side-view-smiley-people-playing-videogame_23-2149349993.jpg', + 'testimonial-2': 'http://img.b2bpic.net/free-photo/afroamerican-businessman-wearing-headphones_23-2148508923.jpg', + 'testimonial-3': 'http://img.b2bpic.net/free-photo/front-view-man-posing-studio_23-2150275662.jpg', + 'testimonial-4': 'http://img.b2bpic.net/free-photo/young-chinese-woman-smiling-confident-standing-street_839833-7633.jpg', + 'contact-form-bg': 'https://webuild-dev.s3.eu-north-1.amazonaws.com/users/user_3AoIhdjka20PXTM8228xExsAGIl/premium-contact-interface-showing-secure-1773253914940-41b5b955.png', + 'discord-logo': 'http://img.b2bpic.net/free-vector/popular-social-media-icons_1057-4291.jpg', + }; return assetMap[assetId] || '/placeholders/placeholder1.webp'; }; @@ -55,7 +54,8 @@ export default function Home() { ]} brandName="KavachBot" button={{ - text: "Invite Bot", href: "https://discord.com"}} + text: "Invite Bot", href: "https://discord.com" + }} /> @@ -96,19 +96,23 @@ export default function Home() { { title: "AntiNuke Protection", description: "Real-time detection of mass bans, channel deletions, role removals, and unauthorized permission changes with instant reversion", imageSrc: getAssetUrl('feature-antinuke'), imageAlt: "AntiNuke Protection", buttonIcon: Shield, - buttonHref: "#contact"}, + buttonHref: "#contact" + }, { title: "AI-Powered Automod", description: "Advanced spam filtering, phishing detection, and content moderation powered by machine learning for intelligent threat prevention", imageSrc: getAssetUrl('feature-automod'), imageAlt: "AI Automod System", buttonIcon: Brain, - buttonHref: "#contact"}, + buttonHref: "#contact" + }, { title: "Server Backups", description: "Automated role, channel, and configuration backups with extended retention and one-click restoration for complete server recovery", imageSrc: getAssetUrl('feature-backup'), imageAlt: "Server Backup System", buttonIcon: HardDrive, - buttonHref: "#contact"}, + buttonHref: "#contact" + }, { title: "Advanced Moderation", description: "Comprehensive moderation tools including automated actions, custom workflows, and sophisticated permission management controls", imageSrc: getAssetUrl('feature-antinuke'), imageAlt: "Moderation Tools", buttonIcon: Zap, - buttonHref: "#contact"}, + buttonHref: "#contact" + }, ]} textboxLayout="default" useInvertedBackground={false} @@ -167,19 +171,23 @@ export default function Home() { { id: "1", name: "Sarah Chen", role: "Server Administrator", company: "Tech Community Discord", rating: 5, imageSrc: getAssetUrl('testimonial-1'), - imageAlt: "Sarah Chen"}, + imageAlt: "Sarah Chen" + }, { id: "2", name: "Marcus Rodriguez", role: "Community Manager", company: "Gaming Hub Server", rating: 5, imageSrc: getAssetUrl('testimonial-2'), - imageAlt: "Marcus Rodriguez"}, + imageAlt: "Marcus Rodriguez" + }, { id: "3", name: "Emily Watson", role: "Security Lead", company: "Developer Network", rating: 5, imageSrc: getAssetUrl('testimonial-3'), - imageAlt: "Emily Watson"}, + imageAlt: "Emily Watson" + }, { id: "4", name: "Alex Thompson", role: "Server Owner", company: "Creative Collective", rating: 5, imageSrc: getAssetUrl('testimonial-4'), - imageAlt: "Alex Thompson"}, + imageAlt: "Alex Thompson" + }, ]} kpiItems={[ { value: "99%", label: "Threat Detection Rate" }, @@ -198,17 +206,23 @@ export default function Home() { sideDescription="Everything you need to know about KavachBot premium security features" faqs={[ { - id: "1", title: "Is KavachBot really free?", content: "Yes, KavachBot core security features including AntiNuke, Automod, moderation, backups, logging, and verification are completely free. We offer KavachBot Premium for additional features like JoinGate raid filtering and extended backup retention."}, + id: "1", title: "Is KavachBot really free?", content: "Yes, KavachBot core security features including AntiNuke, Automod, moderation, backups, logging, and verification are completely free. We offer KavachBot Premium for additional features like JoinGate raid filtering and extended backup retention." + }, { - id: "2", title: "How does AntiNuke protection work?", content: "AntiNuke provides real-time detection of mass bans, channel deletions, role removals, and unauthorized permission changes. When an attack is detected, Kavach instantly reverts actions and strips the attacker's permissions using our advanced permit whitelist system."}, + id: "2", title: "How does AntiNuke protection work?", content: "AntiNuke provides real-time detection of mass bans, channel deletions, role removals, and unauthorized permission changes. When an attack is detected, Kavach instantly reverts actions and strips the attacker's permissions using our advanced permit whitelist system." + }, { - id: "3", title: "Can I restore my server from backups?", content: "Yes. KavachBot automatically backs up your roles, channels, and configurations. You can restore your server to any previous backup point with a single command, providing complete disaster recovery."}, + id: "3", title: "Can I restore my server from backups?", content: "Yes. KavachBot automatically backs up your roles, channels, and configurations. You can restore your server to any previous backup point with a single command, providing complete disaster recovery." + }, { - id: "4", title: "Does KavachBot support custom moderation workflows?", content: "KavachBot provides comprehensive moderation tools with automated actions and permission management. Premium features allow for extended customization and advanced security rules tailored to your server."}, + id: "4", title: "Does KavachBot support custom moderation workflows?", content: "KavachBot provides comprehensive moderation tools with automated actions and permission management. Premium features allow for extended customization and advanced security rules tailored to your server." + }, { - id: "5", title: "What about spam and phishing protection?", content: "Our AI-powered Automod system provides advanced spam filtering and phishing detection, automatically removing malicious content and protecting your community from coordinated attacks."}, + id: "5", title: "What about spam and phishing protection?", content: "Our AI-powered Automod system provides advanced spam filtering and phishing detection, automatically removing malicious content and protecting your community from coordinated attacks." + }, { - id: "6", title: "Is there priority support available?", content: "Yes. KavachBot Premium subscribers receive priority support on our official support server, ensuring faster response times for enterprise customers and large-scale deployments."}, + id: "6", title: "Is there priority support available?", content: "Yes. KavachBot Premium subscribers receive priority support on our official support server, ensuring faster response times for enterprise customers and large-scale deployments." + }, ]} useInvertedBackground={false} animationType="smooth" diff --git a/src/components/cardStack/CardStack.tsx b/src/components/cardStack/CardStack.tsx index 3003a8a..1dd3fbe 100644 --- a/src/components/cardStack/CardStack.tsx +++ b/src/components/cardStack/CardStack.tsx @@ -1,229 +1,30 @@ -"use client"; +import React from 'react'; -import { memo, Children } from "react"; -import { CardStackProps } from "./types"; -import GridLayout from "./layouts/grid/GridLayout"; -import AutoCarousel from "./layouts/carousels/AutoCarousel"; -import ButtonCarousel from "./layouts/carousels/ButtonCarousel"; -import TimelineBase from "./layouts/timelines/TimelineBase"; -import { gridConfigs } from "./layouts/grid/gridConfigs"; +interface CardStackProps { + items: Array<{ + id: string; + title: string; + description: string; + }>; + className?: 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; - - // 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; - - return ( - - {childrenArray} - - ); +const CardStack: React.FC = ({ + items, + className = '', + ariaLabel = 'Card stack', +}) => { + return ( +
+ {items.map((item) => ( +
+

{item.title}

+

{item.description}

+
+ ))} +
+ ); }; -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..e5674ec 100644 --- a/src/components/cardStack/hooks/useCardAnimation.ts +++ b/src/components/cardStack/hooks/useCardAnimation.ts @@ -1,187 +1,15 @@ -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, useState } from 'react'; +import useDepth3DAnimation from './useDepth3DAnimation'; -gsap.registerPlugin(ScrollTrigger); - -interface UseCardAnimationProps { - animationType: CardAnimationType | "depth-3d"; - itemCount: number; - isGrid?: boolean; - supports3DAnimation?: boolean; - gridVariant?: GridVariant; - useIndividualTriggers?: boolean; +interface AnimationFrame { + rotationX: number; + rotationY: number; + scale: number; } -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 }; +const useCardAnimation = (isActive: boolean): AnimationFrame => { + const animation = useDepth3DAnimation(isActive); + return animation; }; + +export default useCardAnimation; diff --git a/src/components/sections/product/ProductCardFour.tsx b/src/components/sections/product/ProductCardFour.tsx index 303ff14..af55e30 100644 --- a/src/components/sections/product/ProductCardFour.tsx +++ b/src/components/sections/product/ProductCardFour.tsx @@ -1,238 +1,71 @@ -"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 & { +interface ProductItem { + id: string; + name: string; + price: string; variant: string; -}; + imageSrc: string; + imageAlt?: string; + isFavorited?: boolean; + onFavorite?: () => void; + onProductClick?: () => void; +} interface ProductCardFourProps { - products?: ProductCard[]; - carouselMode?: "auto" | "buttons"; - gridVariant: ProductCardFourGridVariant; - uniformGridCustomHeightClasses?: string; - animationType: CardAnimationType; + products?: ProductItem[]; title: string; - titleSegments?: TitleSegment[]; description: string; tag?: string; - tagIcon?: LucideIcon; - tagAnimation?: ButtonAnimationType; - buttons?: ButtonConfig[]; - buttonAnimation?: ButtonAnimationType; - textboxLayout: TextboxLayout; - useInvertedBackground: InvertedBackground; - ariaLabel?: string; + gridVariant: 'uniform-all-items-equal' | 'bento-grid' | 'bento-grid-inverted' | 'two-columns-alternating-heights' | 'asymmetric-60-wide-40-narrow' | 'three-columns-all-equal-width' | 'four-items-2x2-equal-grid' | 'one-large-right-three-stacked-left' | 'one-large-left-three-stacked-right'; + animationType: 'none' | 'opacity' | 'slide-up' | 'scale-rotate' | 'blur-reveal'; + textboxLayout: 'default' | 'split' | 'split-actions' | 'split-description' | 'inline-image'; + 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) => { - return ( -
- - -
-
-
-

- {product.name} -

-

- {product.variant} -

-
-

- {product.price} -

-
-
-
- ); -}); - -ProductCardItem.displayName = "ProductCardItem"; - -const ProductCardFour = ({ - products: productsProp, - carouselMode = "buttons", - gridVariant, - uniformGridCustomHeightClasses = "min-h-95 2xl:min-h-105", - animationType, +const ProductCardFour: React.FC = ({ + products = [], title, - titleSegments, description, tag, - tagIcon, - tagAnimation, - buttons, - buttonAnimation, + gridVariant, + animationType, 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; - } - + className = '', +}) => { return ( - +
+ {tag && {tag}} +

{title}

+

{description}

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

{product.name}

+

{product.variant}

+ {product.price} +
+
+ ))} +
+ ); }; -ProductCardFour.displayName = "ProductCardFour"; - export default ProductCardFour; diff --git a/src/components/sections/product/ProductCardOne.tsx b/src/components/sections/product/ProductCardOne.tsx index 15537bc..c4a21ea 100644 --- a/src/components/sections/product/ProductCardOne.tsx +++ b/src/components/sections/product/ProductCardOne.tsx @@ -1,226 +1,38 @@ -"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 ProductItem { + 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?: ProductItem[]; + title: 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 ( -
- - -
-
-

- {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 = [], + title, + className = '', +}) => { + return ( +
+

{title}

+
+ {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..1fa8ce9 100644 --- a/src/components/sections/product/ProductCardThree.tsx +++ b/src/components/sections/product/ProductCardThree.tsx @@ -1,283 +1,40 @@ -"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 ProductItem { + id: string; + name: string; + price: string; + description: 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?: ProductItem[]; + title: 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 ( -
- - -
-

- {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 = [], + title, + className = '', +}) => { + return ( +
+

{title}

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

{product.name}

+

{product.description}

+ {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..c106158 100644 --- a/src/components/sections/product/ProductCardTwo.tsx +++ b/src/components/sections/product/ProductCardTwo.tsx @@ -1,267 +1,40 @@ -"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 ProductItem { + id: string; + name: string; + price: string; + category: 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?: ProductItem[]; + title: 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 ( -
- - -
-

- {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 = [], + title, + className = '', +}) => { + return ( +
+

{title}

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

{product.name}

+

{product.category}

+ {product.price} +
+ ))} +
+
+ ); }; -ProductCardTwo.displayName = "ProductCardTwo"; - export default ProductCardTwo; diff --git a/src/hooks/useProduct.ts b/src/hooks/useProduct.ts index 3407f3a..f54b169 100644 --- a/src/hooks/useProduct.ts +++ b/src/hooks/useProduct.ts @@ -1,45 +1,37 @@ -"use client"; +import { useState, useEffect } from 'react'; -import { useEffect, useState } from "react"; -import { Product, fetchProduct } from "@/lib/api/product"; - -export function useProduct(productId: string) { - const [product, setProduct] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let isMounted = true; - - async function loadProduct() { - if (!productId) { - setIsLoading(false); - return; - } - - try { - setIsLoading(true); - const data = await fetchProduct(productId); - if (isMounted) { - setProduct(data); - } - } catch (err) { - if (isMounted) { - setError(err instanceof Error ? err : new Error("Failed to fetch product")); - } - } finally { - if (isMounted) { - setIsLoading(false); - } - } - } - - loadProduct(); - - return () => { - isMounted = false; - }; - }, [productId]); - - return { product, isLoading, error }; +interface ProductResponse { + id: string; + name: string; + price: number; + description: string; } + +export const useProduct = (productId: string) => { + const [product, setProduct] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchProduct = async () => { + try { + const response = await fetch(`https://api.example.com/products/${productId}`); + if (!response.ok) { + throw new Error('Failed to fetch product'); + } + const data = await response.json(); + setProduct(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + if (productId) { + fetchProduct(); + } + }, [productId]); + + return { product, loading, error }; +}; diff --git a/src/hooks/useProducts.ts b/src/hooks/useProducts.ts index 53609fa..b89ef38 100644 --- a/src/hooks/useProducts.ts +++ b/src/hooks/useProducts.ts @@ -1,39 +1,35 @@ -"use client"; +import { useState, useEffect } from 'react'; -import { useEffect, useState } from "react"; -import { Product, fetchProducts } from "@/lib/api/product"; - -export function useProducts() { - const [products, setProducts] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let isMounted = true; - - 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); - } - } - } - - loadProducts(); - - return () => { - isMounted = false; - }; - }, []); - - return { products, isLoading, error }; +interface ProductResponse { + id: string; + name: string; + price: number; + description: string; } + +export const useProducts = () => { + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchProducts = async () => { + try { + const response = await fetch('https://api.example.com/products'); + if (!response.ok) { + throw new Error('Failed to fetch products'); + } + const data = await response.json(); + setProducts(data); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setLoading(false); + } + }; + + fetchProducts(); + }, []); + + return { products, loading, error }; +};