Skip to main content
Glama
PricingCarousel.tsx12.6 kB
'use client'; import { formatOnboardUrl } from '@components/OnboardPage/formatOnboardUrl'; import { Steps } from '@components/OnboardPage/steps'; import type { GetPricingResult } from '@intlayer/backend'; import { useGetPricing, useUser } from '@intlayer/design-system/hooks'; import { useSearchParams } from 'next/navigation'; import { useIntlayer } from 'next-intlayer'; import React, { type FC, type HTMLAttributes, type MouseEventHandler, type TouchEventHandler, useEffect, useRef, useState, } from 'react'; import { type Period, Plans } from './data.content'; import { PricingColumn } from './PricingColumn'; type PricingCarouselProps = HTMLAttributes<HTMLDivElement> & { pricings: GetPricingResult['data']; focusedPeriod: Period; setFocusedPeriod: (period: Period) => void; }; const plans: Plans[] = [Plans.Free, Plans.Premium, Plans.Enterprise]; const IS_SELECT_PLAN_ON_HOVER = false; const SELECT_PLAN_ON_HOVER_TIMEOUT = 1000; const getPrice = (price: number, period: Period): number => { if (period === 'yearly') { return price / 12 / 100; } return price / 100; }; /** * PricingCarousel component * @param props - React props * @param focusedPeriod - Currently selected pricing period (e.g. 'monthly', 'yearly') * @param setFocusedPeriod - Function to update the focused pricing period * @returns - PricingCarousel component for the pricing plans */ export const PricingCarousel: FC<PricingCarouselProps> = ({ focusedPeriod, setFocusedPeriod, pricings, ...props }) => { const { user } = useUser(); const searchParams = useSearchParams(); const allParams = Object.fromEntries(searchParams.entries()); const { pricing, period } = useIntlayer('pricing'); const { data: pricingData, isFetching: isLoading } = useGetPricing( { priceIds: [ process.env.NEXT_PUBLIC_STRIPE_PREMIUM_YEARLY_PRICE_ID!, process.env.NEXT_PUBLIC_STRIPE_PREMIUM_MONTHLY_PRICE_ID!, process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_YEARLY_PRICE_ID!, process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID!, ], promoCode: allParams.promoCode, }, { enabled: Boolean(allParams.promoCode), } ); const [selectedPlanIndex, setSelectedPlanIndex] = useState<number | null>( null ); // Index of selected plan, starting as null const [displayedPlanIndex, setDisplayedPlanIndex] = useState<number>( selectedPlanIndex ?? 0 ); // Index of the plan currently displayed const [startX, setStartX] = useState(0); // Stores the start position of a swipe or drag event const [isDragging, setIsDragging] = useState(false); // Indicates if a dragging action is happening const [isClicked, setIsClicked] = useState(false); // Tracks if a plan has been clicked to prevent selection on hover const [focusTimeout, setFocusTimeout] = useState<NodeJS.Timeout | null>(null); // Timeout for delayed plan selection on hover const columnRef = useRef<HTMLDivElement>(null); // Reference to the pricing column div element /** * Handle switching plans based on swipe or drag distance * @param diff - Difference in X coordinate from swipe/drag start */ const handleSwitchPlan = (diff: number) => { const screenWidth = window.innerWidth; const swipeStep = screenWidth / 5; // Defines how much distance must be swiped to switch plans const numSwipe = Math.round(diff / swipeStep); if (Math.abs(numSwipe) >= 1) { const newIndex = (displayedPlanIndex ?? displayedPlanIndex) - numSwipe; // Update the selected plan index if within valid range if (newIndex >= 0 && newIndex < plans.length) { setSelectedPlanIndex(newIndex); setStartX((prev) => prev + diff); } } }; /** Mouse event handlers for drag functionality */ const handleMouseDown: MouseEventHandler<HTMLDivElement> = (e) => { setIsDragging(true); setStartX(e.clientX); // Store the initial X position on mouse down }; const handleMouseMove: MouseEventHandler<HTMLDivElement> = (e) => { if (isDragging) { const diff = e.clientX - startX; // Calculate the difference from the start position handleSwitchPlan(diff); } }; const handleMouseUp: MouseEventHandler<HTMLDivElement> = () => { setIsDragging(false); // Stop dragging }; /** Touch event handlers for swipe functionality */ const handleTouchStart: TouchEventHandler = (e) => { setIsDragging(true); setStartX(e.touches[0].clientX ?? 0); // Store the initial X position on touch start }; const handleTouchMove: TouchEventHandler = (e) => { if (isDragging) { const diff = e.touches[0].clientX - startX; // Calculate the difference from the start position handleSwitchPlan(diff); } }; const handleTouchEnd: TouchEventHandler = () => { setIsDragging(false); // Stop dragging }; /** * Set a delay before selecting a plan to prevent immediate selection on hover * @param index - Index of the plan to be selected */ const selectPlanAfterDelay = (index: number) => { if (!IS_SELECT_PLAN_ON_HOVER) return; // Clear existing timeout if it exists if (focusTimeout) { clearTimeout(focusTimeout); } // Set new timeout to select plan after 0.5 seconds const newTimeout = setTimeout(() => { setSelectedPlanIndex(index); }, SELECT_PLAN_ON_HOVER_TIMEOUT); setFocusTimeout(newTimeout); }; /** * Handle mouse enter event to set delayed plan selection * @param index - Index of the plan to be selected * @returns Mouse event handler function */ const handleMouseEnter = (index: number) => (e: React.MouseEvent) => { e.stopPropagation(); if (isClicked) return; // Prevent hover selection if plan is clicked selectPlanAfterDelay(index); }; /** * Ensure selected plan index stays within bounds of available plans */ useEffect(() => { if (selectedPlanIndex === null) return; if (selectedPlanIndex < 0) { setSelectedPlanIndex(0); } else if (selectedPlanIndex > plans.length - 1) { setSelectedPlanIndex(plans.length - 1); } }, [selectedPlanIndex]); /** * Smoothly transition between plans if the selected plan index changes */ useEffect(() => { if (selectedPlanIndex === null) return; let interval: NodeJS.Timeout; if (selectedPlanIndex !== displayedPlanIndex) { interval = setInterval(() => { setDisplayedPlanIndex((prev) => { if (prev === selectedPlanIndex) { clearInterval(interval); return prev; } return prev < selectedPlanIndex ? prev + 1 : prev - 1; }); }, 50); // Smoothly adjust index in steps } return () => { if (interval) { clearInterval(interval); } }; }, [selectedPlanIndex, displayedPlanIndex]); /** * Set column styling based on its position relative to the displayed plan * @param index - Index of the plan column * @param displayedIndex - Index of the displayed plan column * @returns - Tailwind CSS class for styling the column */ const setColumnStyle = (index: number, displayedIndex: number) => { const diff = Math.abs(index - displayedIndex); switch (diff) { case 0: return 'opacity-100 z-40'; case 1: return 'opacity-75 z-30'; case 2: return 'opacity-50 z-20'; default: return 'opacity-0 z-10'; } }; /** * Set column scale based on its position relative to the displayed plan * @param index - Index of the plan column * @param displayedIndex - Index of the displayed plan column * @returns - Scale value for the column */ const setColumnScale = (index: number, displayedIndex: number) => { const diff = Math.abs(index - displayedIndex); switch (diff) { case 0: return 1; case 1: return 0.9; case 2: return 0.8; default: return 0.7; } }; /** * Set column X position based on its index relative to the displayed plan * @param index - Index of the plan column * @param displayedIndex - Index of the displayed plan column * @returns - Calculated X position for the column */ const setColumnPositionX = (index: number, displayedIndex: number) => { const diff = index - displayedIndex; const screenWidth = window.innerWidth; const offset = (3 * screenWidth) / 10; return diff * offset; }; // Set initial displayed plan index if none is selected useEffect(() => { if (!selectedPlanIndex) { setDisplayedPlanIndex(1); } }, [selectedPlanIndex]); // Clear the focus timeout on component unmount useEffect(() => { return () => { if (focusTimeout) { clearTimeout(focusTimeout); } }; }, [focusTimeout]); return ( <div className="relative h-screen w-full cursor-grab select-none overflow-hidden active:cursor-grabbing" onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd} role="button" tabIndex={0} data-testid="pricing-carousel" style={{ height: (columnRef?.current?.offsetHeight ?? 0) + 30, }} {...props} > {plans.map((plan, index) => ( <div className={`-translate-x-1/2 absolute left-1/2 transition duration-300 ease-in-out${setColumnStyle( index, displayedPlanIndex )}`} ref={columnRef} role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { setSelectedPlanIndex(index); // Handle Enter/Space to activate } if (e.key === 'ArrowLeft') { setSelectedPlanIndex( (prev) => Math.max(0, (prev ?? 0) - 1) // Handle Left Arrow to go back ); } if (e.key === 'ArrowRight') { setSelectedPlanIndex( (prev) => Math.min(plans.length - 1, (prev ?? 0) + 1) // Handle Right Arrow to go forward ); } }} aria-pressed={selectedPlanIndex === index} // Indicates selected state key={plan} style={ columnRef?.current?.offsetWidth ? { transform: ` translateX(${setColumnPositionX(index, displayedPlanIndex)}px) scale(${setColumnScale(index, displayedPlanIndex)}) `, } : undefined } onClick={(e) => { e.stopPropagation(); setIsClicked(true); setSelectedPlanIndex(index); }} onMouseEnter={handleMouseEnter(index)} > <PricingColumn unit="$" period={period.monthly.value} isPriceLoading={isLoading} totalPrice={getPrice( pricingData?.data?.[ (pricing[focusedPeriod][plan] as any).priceId?.value ]?.finalTotal ?? pricings?.[(pricing[focusedPeriod][plan] as any).priceId?.value] ?.finalTotal ?? 0, focusedPeriod )} basePrice={getPrice( pricingData?.data?.[ (pricing[focusedPeriod][plan] as any).priceId?.value ]?.originalTotal ?? pricings?.[(pricing[focusedPeriod][plan] as any).priceId?.value] ?.originalTotal ?? 0, focusedPeriod )} checkPoint={pricing[focusedPeriod][plan].checkPoint.map( (el) => el.value )} callToActionLabel={ pricing[focusedPeriod][plan].callToAction.label.value } callToActionText={ pricing[focusedPeriod][plan].callToAction.text.value } callToActionUrl={formatOnboardUrl({ plan, period: focusedPeriod, step: user ? Steps.SetupOrganization : Steps.Registration, otherParams: allParams, })} title={pricing[focusedPeriod][plan].title.value} description={pricing[focusedPeriod][plan].description.value} className={displayedPlanIndex !== index ? 'hover:scale-103' : ''} /> </div> ))} </div> ); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server