'use client'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import Link from 'next/link'; import { Trans, msg } from '@lingui/macro'; import { useLingui } from '@lingui/react'; import type { AutoplayType } from 'embla-carousel-autoplay'; import Autoplay from 'embla-carousel-autoplay'; import useEmblaCarousel from 'embla-carousel-react'; import { usePlausible } from 'next-plausible'; import { useTheme } from 'next-themes'; import { Card } from '@documenso/ui/primitives/card'; import { Progress } from '@documenso/ui/primitives/progress'; import { Slide } from './slide'; const SLIDES = [ { label: msg`Signing Process`, type: 'video', srcLight: 'https://github.com/documenso/design/raw/main/marketing/signing.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/signing.webm', }, { label: msg`Teams`, type: 'video', srcLight: 'https://github.com/documenso/design/raw/main/marketing/teams.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/teams.webm', }, { label: 'Zapier', type: 'video', srcLight: 'https://github.com/documenso/design/raw/main/marketing/zapier.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/zapier.webm', }, { label: msg`Direct Link`, type: 'video', srcLight: 'https://github.com/documenso/design/raw/main/marketing/direct-links.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/direct-links.webm', }, { label: 'Webhooks', type: 'video', srcLight: 'https://github.com/documenso/design/raw/main/marketing/webhooks.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/webhooks.webm', }, { label: 'API', type: 'video', srcLight: 'https://github.com/documenso/design/raw/main/marketing/api.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/api.webm', }, { label: msg`Profile`, type: 'video', srcLight: 'https://github.com/documenso/design/raw/main/marketing/profile_teaser.webm', srcDark: 'https://github.com/documenso/design/raw/main/marketing/dark/profile_teaser.webm', }, ]; export const Carousel = () => { const { _ } = useLingui(); const event = usePlausible(); const slides = SLIDES; const [_isPlaying, setIsPlaying] = useState(false); const [selectedIndex, setSelectedIndex] = useState(0); const [progress, setProgress] = useState(0); const videoRefs = useRef<(HTMLVideoElement | null)[]>([]); const [autoplayDelay, setAutoplayDelay] = useState([]); const { resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [ Autoplay({ playOnInit: true, delay: autoplayDelay[selectedIndex] || 5000 }), ]); const [emblaThumbsRef, emblaThumbsApi] = useEmblaCarousel( { loop: true, containScroll: 'keepSnaps', dragFree: true, }, [Autoplay({ playOnInit: true, delay: autoplayDelay[selectedIndex] || 5000 })], ); const onThumbClick = useCallback( (index: number) => { if (!emblaApi || !emblaThumbsApi) { return; } emblaApi.scrollTo(index); }, [emblaApi, emblaThumbsApi], ); const onSelect = useCallback(() => { if (!emblaApi || !emblaThumbsApi) { return; } setSelectedIndex(emblaApi.selectedScrollSnap()); emblaThumbsApi.scrollTo(emblaApi.selectedScrollSnap()); resetProgress(); // moduleResolution: bundler breaks this type // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const autoplay = emblaApi.plugins()?.autoplay as unknown as AutoplayType | undefined; if (autoplay) { autoplay.reset(); } }, [emblaApi, emblaThumbsApi, setSelectedIndex]); const resetProgress = useCallback(() => { setProgress(0); }, []); useEffect(() => { const setVideoDurations = async () => { const durations = await Promise.all( videoRefs.current.map( async (video) => new Promise((resolve) => { if (video) { video.onloadedmetadata = () => resolve(video.duration * 1000); } else { resolve(5000); } }), ), ); setAutoplayDelay(durations); }; void setVideoDurations(); }, [slides, mounted, resolvedTheme]); useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { const video = entry.target as HTMLVideoElement; video .play() .catch((error) => console.log('Error attempting to play the video:', error)); } else { const video = entry.target as HTMLVideoElement; video.pause(); } }); }, { threshold: 0.5, }, ); videoRefs.current.forEach((video) => { if (video) { observer.observe(video); } }); return () => { observer.disconnect(); }; }, [slides, mounted, resolvedTheme]); useEffect(() => { if (!emblaApi) return; onSelect(); emblaApi.on('select', onSelect).on('reInit', onSelect); }, [emblaApi, onSelect, mounted, resolvedTheme]); useEffect(() => { // moduleResolution: bundler breaks this type // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const autoplay = emblaApi?.plugins()?.autoplay as unknown as AutoplayType | undefined; if (!autoplay || !emblaApi) { return; } setIsPlaying(autoplay.isPlaying()); // eslint-disable-next-line @typescript-eslint/no-explicit-any (emblaApi as unknown as any) .on('autoplay:play', () => setIsPlaying(true)) .on('autoplay:stop', () => setIsPlaying(false)) .on('reInit', () => setIsPlaying(autoplay.isPlaying())); }, [emblaApi, mounted, resolvedTheme]); useEffect(() => { if (autoplayDelay[selectedIndex] === undefined) return; const updateInterval = 50; const increment = 100 / (autoplayDelay[selectedIndex] / updateInterval); let progressValue = 0; const timer = setInterval(() => { setProgress((prevProgress) => { progressValue = prevProgress + increment; if (progressValue >= 100) { clearInterval(timer); if (emblaApi) { emblaApi.scrollNext(); } return 100; } return progressValue; }); }, updateInterval); return () => clearInterval(timer); }, [selectedIndex, autoplayDelay, emblaApi, mounted, resolvedTheme]); useEffect(() => { if (!emblaApi) return; const resetCarousel = () => { emblaApi.reInit(); emblaApi.scrollTo(0); }; resetCarousel(); }, [emblaApi, autoplayDelay, mounted, resolvedTheme]); // Ensure the component renders only after mounting to avoid theme issues if (!mounted) return null; return ( <>
{slides.map((slide, index) => (
{slide.type === 'video' && ( )}
))}
{selectedIndex + 1}/{slides.length}
event('view-demo')} > Book a Demo Want to learn more about Documenso and how it works? Book a demo today! Our founders will walk you through the application and answer any questions you may have regarding usage, integration, and more.
{slides.map((slide, index) => ( onThumbClick(index)} selected={index === selectedIndex} index={index} label={typeof slide.label === 'string' ? slide.label : _(slide.label)} /> ))}
); };