nx/nx-dev/ui-enterprise/src/lib/testimonial-carousel.tsx
Benjamin Cabanes a279bf6df3
docs(nx-dev): add Hetzner cloud testimonial section to homepage (#29858)
Introduced a new "Hetzner Cloud Testimonial" component showcasing a featured client story with video support. The `video-modal.tsx` component was moved to `ui-common` for reuse, and the homepage was updated to display it while commenting out the previous "Trusted By" section.
2025-02-03 15:16:02 -05:00

237 lines
10 KiB
TypeScript

import { ReactElement, ReactNode, useRef, useState } from 'react';
import {
CarouselHandle,
CarouselRoot,
CarouselSlide,
CarouselViewport,
} from './carousel';
import { PayfitIcon, UkgIcon } from '@nx/nx-dev/ui-icons';
import { PlayIcon } from '@heroicons/react/24/outline';
import { sendCustomEvent } from '@nx/nx-dev/feature-analytics';
import { VideoModal } from '@nx/nx-dev/ui-common';
export function Carousel({
items,
}: {
items: { element: ReactNode; innerButtonElement: ReactNode }[];
}): ReactElement {
const carouselRef = useRef<CarouselHandle>(null);
const iterableItems = items.map((item, index) => ({
...item,
id: crypto.randomUUID(),
}));
const [currentIndex, setCurrentIndex] = useState(0);
const handleSlideChange = (index: number) => setCurrentIndex(index);
return (
<div className="w-full">
{/* Main carousel section */}
<CarouselRoot
className="overflow-hidden rounded-lg xl:[box-shadow:0_50px_100px_-20px_rgba(50,50,93,0.25),_0_30px_60px_-30px_rgba(0,0,0,0.3)]"
onSlideChange={handleSlideChange}
enableKeyboardNavigation={true}
autoPlayInterval={6000}
ref={carouselRef}
>
<CarouselViewport>
{iterableItems.map((item, index) => (
<CarouselSlide key={item.id}>{item.element}</CarouselSlide>
))}
</CarouselViewport>
{/* Custom line-style indicators */}
<div className="absolute bottom-8 left-1/2 flex -translate-x-1/2 gap-2">
{iterableItems.map((_, index) => (
<div
key={index}
className={`h-1 w-8 rounded-full transition-colors duration-300 ${
index === currentIndex ? 'bg-white' : 'bg-white/30'
}`}
/>
))}
</div>
</CarouselRoot>
{/* Partner logos section - now linked to carousel items */}
<div className="mt-12 flex items-center justify-center divide-x divide-slate-200 dark:divide-slate-700">
{iterableItems.map((item, index) => (
<button
key={item.id + '-logo'}
onClick={() => carouselRef.current?.goToSlide(index)}
className={`px-8 py-4 transition-all duration-300 ${
index === currentIndex
? 'underline opacity-100 grayscale-0'
: 'opacity-50 grayscale'
}`}
>
{item.innerButtonElement}
</button>
))}
</div>
</div>
);
}
export function TestimonialCarousel(): ReactElement {
const [isOpen, setIsOpen] = useState(false);
const [currentVideo, setCurrentVideo] = useState('');
const openVideo = (videoUrl: string) => {
setCurrentVideo(videoUrl);
setIsOpen(true);
};
return (
<section className="">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
<Carousel
items={[
{
element: (
<div
onClick={() => {
openVideo('https://youtu.be/Vdk-tza4PCs');
sendCustomEvent(
'payfit-testimonial-video-click',
'testimonial-carousel',
'enterprise'
);
}}
className="group relative cursor-pointer overflow-hidden"
>
<div
className="absolute inset-0 bg-opacity-75 bg-cover bg-center bg-no-repeat transition-transform duration-700 group-hover:scale-105"
style={{
backgroundImage:
"url('https://images.unsplash.com/photo-1511376868136-742c0de8c9a8?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-r from-[#0F6FDE] via-[#0F6FDE] via-70% to-[#0F6FDE]/40" />
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/60 opacity-0 backdrop-blur-sm transition duration-300 group-hover:opacity-100 dark:bg-slate-950/60">
<div className="flex items-center gap-2 text-lg font-semibold text-slate-950 drop-shadow dark:text-white">
<PlayIcon className="size-8" />
Watch the interview
</div>
</div>
<div className="relative mx-auto grid max-w-2xl grid-cols-1 px-12 py-16 text-white lg:mx-0 lg:max-w-none lg:grid-cols-4">
<div className="col-span-3 flex flex-col">
<figure className="flex flex-auto flex-col justify-between">
<blockquote className="text-pretty text-xl/8">
<p>
"The number of hours we spent trying to manage CI
before, trying to load balance in CircleCI, the
number of agents that we run ourselves by hand and
try to distribute ourselves manually - it was
painful, we'd spend hours and days trying to do
that.{' '}
<span className="font-semibold">
With Nx Cloud we don't need to think about that,
here is my task, deal with it and make it fast
</span>
."
</p>
</blockquote>
<figcaption className="mt-10 flex items-center gap-x-6">
<img
alt="avatar"
src="https://avatars.githubusercontent.com/u/7281023?v=4"
className="size-14 rounded-full bg-slate-50"
/>
<div className="text-base">
<div className="font-semibold">
Nicolas Beaussart
</div>
<div className="mt-1">
Staff Platform Engineer, Payfit
</div>
</div>
</figcaption>
</figure>
</div>
<div className="grid-col-1 grid place-items-center p-4">
<PayfitIcon
aria-hidden="true"
className="size-12 lg:size-20"
/>
</div>
</div>
</div>
),
innerButtonElement: (
<span className="text-2xl">Increase speed</span>
),
},
{
element: (
<div
onClick={() => {
openVideo('https://youtu.be/rSC8wihnfP4');
sendCustomEvent(
'ukg-testimonial-video-click',
'testimonial-carousel',
'enterprise'
);
}}
className="group relative cursor-pointer overflow-hidden"
>
<div
className="absolute inset-0 bg-opacity-75 bg-cover bg-center bg-no-repeat transition-transform duration-700 group-hover:scale-105"
style={{
backgroundImage:
"url('https://images.unsplash.com/photo-1552664730-d307ca884978?q=80&w=2670&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D')",
}}
/>
<div className="absolute inset-0 bg-gradient-to-r from-[#005151] via-[#005151] via-55% to-[#005151]/40" />
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/60 opacity-0 backdrop-blur-sm transition duration-300 group-hover:opacity-100 dark:bg-slate-950/60">
<div className="flex items-center gap-2 text-lg font-semibold text-slate-950 drop-shadow dark:text-white">
<PlayIcon className="size-8" />
Watch the interview
</div>
</div>
<div className="relative mx-auto grid max-w-2xl grid-cols-1 px-12 py-16 text-white lg:mx-0 lg:max-w-none lg:grid-cols-4">
<div className="col-span-2 flex flex-col">
<figure className="flex flex-auto flex-col justify-between">
<blockquote className="text-pretty text-xl/8">
<p>
"I really like the Nx check-ins - Nx people are very
well prepared for how to help their team grow and
scale and to help us spot some of our challenges. I
can't see a future where we don't have Nx."
</p>
</blockquote>
<figcaption className="mt-10 flex items-center gap-x-6">
<img
alt=""
src="https://avatars.githubusercontent.com/u/6657673?v=4"
className="size-14 rounded-full bg-slate-50"
/>
<div className="text-base">
<div className="font-semibold">Sid Govindaraju</div>
<div className="mt-1">Engineering Manager, UKG</div>
</div>
</figcaption>
</figure>
</div>
<div className="grid-col-1 grid place-items-center p-4">
<UkgIcon aria-hidden="true" className="h-6 lg:h-12" />
</div>
</div>
</div>
),
innerButtonElement: (
<span className="text-2xl">Proactive partnership</span>
),
},
]}
/>
</div>
<VideoModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
videoUrl={currentVideo}
/>
</section>
);
}