UNPKG

react-product-video-showcase

Version:

A React component to embed YouTube and Facebook product videos with full control options.

1 lines 55.4 kB
{"version":3,"sources":["../src/components/ProductGrid.tsx","../src/utils/videoConfig.ts","../src/hooks/useLazyLoad.ts","../src/hooks/useVideoPlatform.ts","../src/components/VideoPlayer.tsx","../src/utils/icons.tsx","../src/components/ProductCard.tsx","../src/components/ProductModalY.tsx","../src/components/ProductModalX.tsx","../src/hooks/useDeviceType.ts","../src/lib/style-injector.ts"],"sourcesContent":["import React, { useEffect, useRef, useState } from \"react\";\nimport { ProductCard } from \"./ProductCard\";\nimport { ProductModalY } from \"./ProductModalY\";\nimport { ProductModalX } from \"./ProductModalX\";\nimport { Product, VideoConfig } from \"../types\";\nimport { useDeviceType } from \"../hooks/useDeviceType\";\nimport { injectStyles } from \"../lib/style-injector\";\n\ninterface Props {\n products: Product[];\n layout: {\n desktop: { column: number; row: number };\n tablet: { column: number; row: number };\n mobile: { column: number; row: number };\n };\n maxItems: number;\n videoConfig?: Partial<VideoConfig>;\n sectionHeader?: (params: {\n handleNext?: () => void;\n handlePrev?: () => void;\n isSliding?: boolean;\n }) => React.ReactNode;\n contents?: (product: Product) => React.ReactNode;\n buttons?: (product: Product) => React.ReactNode;\n slide?: boolean;\n slideInterval?: number;\n sliderDirection: \"forward\" | \"backward\";\n expandCard?: boolean;\n overlayExpandCard?: boolean;\n expandCardSlide?: \"vertical\" | \"horizontal\";\n}\n\nexport const ProductGrid: React.FC<Props> = ({\n products,\n layout,\n maxItems,\n videoConfig,\n sectionHeader,\n contents,\n buttons,\n slide = false,\n slideInterval = 3000,\n sliderDirection = \"backward\",\n expandCard = false,\n overlayExpandCard = false,\n expandCardSlide = \"vertical\",\n}) => {\n const device = useDeviceType();\n const { column, row } = layout[device];\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const [modalOpen, setModalOpen] = useState<boolean>(false);\n const slicedProducts = products.slice(0, maxItems);\n const itemsPerPage = column * row;\n const totalPages = Math.ceil(slicedProducts.length / itemsPerPage);\n\n const containerRef = useRef<HTMLDivElement>(null);\n const initialPage = sliderDirection === \"backward\" ? totalPages - 1 : 0;\n const [currentPage, setCurrentPage] = useState(initialPage);\n\n useEffect(() => {\n injectStyles();\n }, []);\n\n const scrollToPage = (page: number) => {\n if (containerRef.current) {\n const width = containerRef.current.clientWidth;\n containerRef.current.scrollTo({\n left: page * width,\n behavior: \"smooth\",\n });\n }\n };\n\n useEffect(() => {\n if (sliderDirection === \"backward\") {\n setCurrentPage(totalPages - 1);\n scrollToPage(totalPages - 1);\n } else {\n setCurrentPage(0);\n scrollToPage(0);\n }\n }, [totalPages, sliderDirection]);\n\n const handleNext = () => {\n const nextPage = (currentPage + 1) % totalPages;\n setCurrentPage(nextPage);\n scrollToPage(nextPage);\n };\n\n const handlePrev = () => {\n const prevPage = (currentPage - 1 + totalPages) % totalPages;\n setCurrentPage(prevPage);\n scrollToPage(prevPage);\n };\n\n useEffect(() => {\n if (!slide || modalOpen) return;\n\n const interval = setInterval(() => {\n if (sliderDirection === \"forward\") {\n handleNext();\n } else {\n handlePrev();\n }\n }, slideInterval);\n\n return () => clearInterval(interval);\n }, [slide, slideInterval, sliderDirection, currentPage]);\n\n useEffect(() => {\n if (activeIndex !== null && activeIndex >= slicedProducts.length) {\n setActiveIndex(null);\n }\n }, [slicedProducts, activeIndex]);\n\n return (\n <>\n <div>\n {sectionHeader && (\n <div className=\"rpvs-mb-4\">\n {sectionHeader({\n handleNext,\n handlePrev,\n isSliding: slide,\n })}\n </div>\n )}\n\n <div\n ref={containerRef}\n className=\"rpvs-w-full rpvs-overflow-x-auto rpvs-snap-x rpvs-scroll-smooth rpvs-hide-scrollbar\"\n style={{\n scrollSnapType: \"x mandatory\",\n }}\n >\n <div className=\"rpvs-flex\">\n {[...Array(totalPages)].map((_, pageIndex) => {\n const pageItems = slicedProducts.slice(\n pageIndex * itemsPerPage,\n (pageIndex + 1) * itemsPerPage\n );\n\n return (\n <div\n key={pageIndex}\n className=\"rpvs-w-full rpvs-flex-shrink-0 rpvs-snap-start rpvs-px-2\"\n style={{ width: \"100%\" }}\n >\n <div\n className=\"rpvs-grid rpvs-gap-5\"\n style={{\n gridTemplateColumns: `repeat(${column}, minmax(0, 1fr))`,\n gridTemplateRows: `repeat(${row}, minmax(0, 1fr))`,\n }}\n >\n {pageItems.map((product, index) => (\n <ProductCard\n key={product.id}\n product={product}\n videoConfig={videoConfig}\n contents={contents ? contents(product) : undefined}\n buttons={buttons ? buttons(product) : undefined}\n expandCard={expandCard}\n overlayExpandCard={overlayExpandCard}\n onExpand={() => {\n setActiveIndex(pageIndex * itemsPerPage + index);\n setModalOpen(true);\n }}\n />\n ))}\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {(expandCard || overlayExpandCard) &&\n activeIndex !== null &&\n (expandCardSlide === \"vertical\" ? (\n <ProductModalY\n products={slicedProducts}\n activeIndex={activeIndex}\n onClose={() => setActiveIndex(null)}\n contents={contents}\n buttons={buttons}\n />\n ) : (\n <ProductModalX\n products={slicedProducts}\n activeIndex={activeIndex}\n onClose={() => setActiveIndex(null)}\n contents={contents}\n buttons={buttons}\n />\n ))}\n </>\n );\n};\n","import { VideoConfig } from \"../types\";\n\nexport const defaultVideoConfig: VideoConfig = {\n // for youtube\n autoplay: false,\n mute: false,\n controls: false,\n modestbranding: false,\n rel: false,\n loop: false,\n playlist: false,\n cc_load_policy: false,\n disablekb: false,\n\n // for facebook\n \"data-autoplay\": false,\n \"data-show-text\": false,\n \"data-allowfullscreen\": false,\n \"data-show-captions\": false,\n \"data-allow-script-access\": \"never\",\n};\n\nexport function mergeVideoConfig(\n userConfig?: Partial<VideoConfig>\n): VideoConfig {\n return {\n ...defaultVideoConfig,\n ...userConfig,\n };\n}\n","import { useEffect, useRef, useState } from \"react\";\n\nexport function useLazyLoad<T extends HTMLElement>(): [\n React.RefObject<T>,\n boolean\n] {\n const ref = useRef<T>(null);\n\n const [isVisible, setIsVisible] = useState(false);\n\n useEffect(() => {\n const observer = new IntersectionObserver(\n ([entry]) => {\n if (entry.isIntersecting) setIsVisible(true);\n },\n { threshold: 0.25 }\n );\n if (ref.current) observer.observe(ref.current);\n return () => observer.disconnect();\n }, []);\n\n return [ref, isVisible];\n}\n","import { useMemo } from \"react\";\n\ntype Platform = \"youtube\" | \"facebook\" | \"unsupported\";\n\ninterface Result {\n platform: Platform;\n videoId: string | null;\n embedUrl: string | null;\n error: string | null;\n}\n\nconst FACEBOOK_PATTERNS = [\n /facebook\\.com\\/[^/]+\\/videos\\/(\\d+)/,\n /facebook\\.com\\/watch\\/?\\?v=(\\d+)/,\n /facebook\\.com\\/video\\.php\\?v=(\\d+)/,\n];\n\nfunction extractYouTubeVideoId(url: string): string | null {\n try {\n const parsedUrl = new URL(url);\n if (parsedUrl.hostname.includes(\"youtu.be\")) {\n return parsedUrl.pathname.substring(1);\n }\n if (parsedUrl.hostname.includes(\"youtube.com\")) {\n const searchParams = parsedUrl.searchParams;\n if (searchParams.get(\"v\")) return searchParams.get(\"v\");\n\n const pathSegments = parsedUrl.pathname.split(\"/\");\n if (pathSegments.includes(\"embed\") || pathSegments.includes(\"shorts\")) {\n return pathSegments[pathSegments.length - 1];\n }\n }\n } catch {}\n return null;\n}\n\nfunction getCanonicalFacebookUrl(originalUrl: string, videoId: string): string {\n const pageMatch = originalUrl.match(/facebook\\.com\\/([^/?#]+)\\/videos/);\n const pageSlug = pageMatch?.[1];\n return pageSlug\n ? `https://www.facebook.com/${pageSlug}/videos/${videoId}`\n : `https://www.facebook.com/video.php?v=${videoId}`;\n}\n\nexport function useVideoPlatform(url: string): Result {\n const facebookAppId = process.env.REACT_FACEBOOK_APP_ID;\n\n return useMemo(() => {\n if (!url) {\n return {\n platform: \"unsupported\",\n videoId: null,\n embedUrl: null,\n error: \"Invalid URL\",\n };\n }\n\n // YouTube\n const videoId = extractYouTubeVideoId(url);\n if (videoId) {\n const embedUrl = `https://www.youtube.com/embed/${videoId}`;\n return { platform: \"youtube\", videoId, embedUrl, error: null };\n }\n\n // Facebook\n if (url.includes(\"facebook.com\")) {\n if (url.includes(\"/reel/\")) {\n return {\n platform: \"facebook\",\n videoId: null,\n embedUrl: null,\n error: \"Facebook Reels not supported\",\n };\n }\n\n for (const pattern of FACEBOOK_PATTERNS) {\n const match = url.match(pattern);\n if (match?.[1]) {\n const fbVideoId = match[1];\n const canonicalUrl = getCanonicalFacebookUrl(url, fbVideoId);\n let embedUrl = `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(\n canonicalUrl\n )}`;\n if (facebookAppId) {\n embedUrl += `&appId=${facebookAppId}`;\n }\n return {\n platform: \"facebook\",\n videoId: fbVideoId,\n embedUrl,\n error: null,\n };\n }\n }\n\n return {\n platform: \"facebook\",\n videoId: null,\n embedUrl: null,\n error: \"Facebook video unsupported or private\",\n };\n }\n\n return {\n platform: \"unsupported\",\n videoId: null,\n embedUrl: null,\n error: \"Unsupported platform\",\n };\n }, [url, facebookAppId]);\n}\n","import React from \"react\";\nimport { useLazyLoad } from \"../hooks/useLazyLoad\";\nimport { useVideoPlatform } from \"../hooks/useVideoPlatform\";\nimport { VideoConfig } from \"../types\";\n\ninterface Props {\n videoUrl: string;\n videoConfig?: Partial<VideoConfig>;\n}\n\nexport const VideoPlayer: React.FC<Props> = ({\n videoUrl,\n videoConfig = {},\n}) => {\n const [ref, isVisible] = useLazyLoad<HTMLDivElement>();\n const { platform, videoId, embedUrl, error } = useVideoPlatform(videoUrl);\n\n if (error) {\n return <div className=\"rpvs-text-red-500\">{error}</div>;\n }\n\n const queryParams = new URLSearchParams();\n\n // YouTube Parameters\n if (platform === \"youtube\") {\n if (videoConfig.autoplay) queryParams.set(\"autoplay\", \"1\");\n if (videoConfig.mute) queryParams.set(\"mute\", \"1\");\n if (videoConfig.controls !== undefined)\n queryParams.set(\"controls\", videoConfig.controls ? \"1\" : \"0\");\n if (videoConfig.modestbranding) queryParams.set(\"modestbranding\", \"1\");\n if (videoConfig.rel !== undefined)\n queryParams.set(\"rel\", videoConfig.rel ? \"1\" : \"0\");\n if (videoConfig.loop && videoId) {\n queryParams.set(\"loop\", \"1\");\n queryParams.set(\"playlist\", videoId);\n }\n if (videoConfig.cc_load_policy) queryParams.set(\"cc_load_policy\", \"1\");\n if (videoConfig.disablekb) queryParams.set(\"disablekb\", \"1\");\n }\n\n // Facebook Parameters (these are `data-*` in HTML, but must be added to iframe as query)\n if (platform === \"facebook\") {\n if (videoConfig[\"data-autoplay\"]) queryParams.set(\"autoplay\", \"true\");\n if (videoConfig[\"data-show-text\"] === false)\n queryParams.set(\"show_text\", \"false\");\n if (videoConfig[\"data-allowfullscreen\"])\n queryParams.set(\"allowfullscreen\", \"true\");\n if (videoConfig[\"data-show-captions\"])\n queryParams.set(\"show_captions\", \"true\");\n if (videoConfig[\"data-allow-script-access\"])\n queryParams.set(\n \"allowScriptAccess\",\n videoConfig[\"data-allow-script-access\"]\n );\n }\n\n const finalEmbedUrl = `${embedUrl}${\n embedUrl?.includes(\"?\") ? \"&\" : \"?\"\n }${queryParams.toString()}`;\n\n return (\n <div\n ref={ref}\n className=\"rpvs-min-w-full rpvs-min-h-[50vh] rpvs-aspect-video rpvs-bg-black rpvs-relative rpvs-rounded rpvs-overflow-hidden\"\n >\n {isVisible ? (\n <iframe\n title={`Embedded ${platform} video`}\n loading=\"lazy\"\n src={finalEmbedUrl}\n width=\"100%\"\n height={platform === \"facebook\" ? \"500\" : \"100%\"}\n className={platform === \"youtube\" ? \"rpvs-w-full rpvs-h-full\" : \"\"}\n allow=\"autoplay; encrypted-media\"\n />\n ) : (\n <div className=\"rpvs-w-full rpvs-h-full rpvs-bg-gray-600 rpvs-relative\">\n <div className=\"rpvs-absolute rpvs-inset-0 rpvs-flex rpvs-items-center rpvs-justify-center\">\n <div className=\"rpvs-w-12 rpvs-h-12 rpvs-border-4 rpvs-border-white rpvs-border-t-transparent rpvs-rounded-full rpvs-animate-spin\" />\n </div>\n </div>\n )}\n </div>\n );\n};\n","import { SVGProps } from \"react\";\n\nexport const Maximize = (props: SVGProps<SVGSVGElement>) => (\n <svg {...props} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M8 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V8M8 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V16M21 8V7.8C21 6.11984 21 5.27976 20.673 4.63803C20.3854 4.07354 19.9265 3.6146 19.362 3.32698C18.7202 3 17.8802 3 16.2 3H16M21 16V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H16\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n);\n\nexport const Minimize = (props: SVGProps<SVGSVGElement>) => (\n <svg {...props} fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path\n d=\"M2.99988 8H3.19988C4.88004 8 5.72011 8 6.36185 7.67302C6.92634 7.3854 7.38528 6.92646 7.6729 6.36197C7.99988 5.72024 7.99988 4.88016 7.99988 3.2V3M2.99988 16H3.19988C4.88004 16 5.72011 16 6.36185 16.327C6.92634 16.6146 7.38528 17.0735 7.6729 17.638C7.99988 18.2798 7.99988 19.1198 7.99988 20.8V21M15.9999 3V3.2C15.9999 4.88016 15.9999 5.72024 16.3269 6.36197C16.6145 6.92646 17.0734 7.3854 17.6379 7.67302C18.2796 8 19.1197 8 20.7999 8H20.9999M15.9999 21V20.8C15.9999 19.1198 15.9999 18.2798 16.3269 17.638C16.6145 17.0735 17.0734 16.6146 17.6379 16.327C18.2796 16 19.1197 16 20.7999 16H20.9999\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n);\n\nexport const ArrowUp = (props: SVGProps<SVGSVGElement>) => (\n <svg\n {...props}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M12 19V5M5 12l7-7 7 7\" />\n </svg>\n);\n\nexport const ArrowDown = (props: SVGProps<SVGSVGElement>) => (\n <svg\n {...props}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M12 5v14M19 12l-7 7-7-7\" />\n </svg>\n);\n\nexport const ArrowLeft = (props: SVGProps<SVGSVGElement>) => (\n <svg\n {...props}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M19 12H5M12 5l-7 7 7 7\" />\n </svg>\n);\n\nexport const ArrowRight = (props: SVGProps<SVGSVGElement>) => (\n <svg\n {...props}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M5 12h14M12 5l7 7-7 7\" />\n </svg>\n);\n\nexport const Close = (props: SVGProps<SVGSVGElement>) => (\n <svg\n {...props}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n viewBox=\"0 0 24 24\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path d=\"M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4 L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1 c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1 c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z\" />\n </svg>\n);\n","import React from \"react\";\nimport { mergeVideoConfig } from \"../utils/videoConfig\";\nimport { VideoPlayer } from \"./VideoPlayer\";\nimport { Product, VideoConfig } from \"../types\";\nimport { Maximize } from \"../utils/icons\";\n\ninterface Props {\n product: Product;\n slide?: boolean;\n contents?: React.ReactNode;\n buttons?: React.ReactNode;\n videoConfig?: Partial<VideoConfig>;\n expandCard?: boolean;\n overlayExpandCard?: boolean;\n onExpand: () => void;\n}\n\nexport const ProductCard: React.FC<Props> = ({\n product,\n slide = false,\n contents,\n buttons,\n videoConfig,\n expandCard,\n overlayExpandCard,\n onExpand,\n}) => {\n const finalConfig = mergeVideoConfig(videoConfig);\n\n return (\n <div\n className={`rpvs-flex rpvs-flex-col rpvs-rounded rpvs-overflow-hidden ${\n slide ? \"rpvs-min-w-[250px] rpvs-max-w-sm\" : \"rpvs-w-full\"\n }`}\n >\n {/* Video area with aspect ratio */}\n <div className=\"rpvs-relative rpvs-aspect-[4/5] md:rpvs-aspect-[5/6] rpvs-w-full rpvs-bg-black rpvs-flex rpvs-items-center rpvs-justify-center\">\n <VideoPlayer videoUrl={product.videoUrl} videoConfig={finalConfig} />\n\n {expandCard && (\n <button\n type=\"button\"\n title=\"Expand\"\n className=\"rpvs-absolute rpvs-top-0 rpvs-right-0 rpvs-text-white rpvs-p-1 rpvs-rounded rpvs-cursor-pointer\"\n onClick={onExpand}\n aria-label=\"Expand video\"\n >\n <Maximize className=\"rpvs-text-white rpvs-w-8 rpvs-h-8\" />\n </button>\n )}\n\n {overlayExpandCard && (\n <button\n type=\"button\"\n aria-label=\"Expand card\"\n title=\"Expand\"\n className=\"rpvs-absolute rpvs-inset-0 rpvs-z-10 rpvs-bg-transparent rpvs-cursor-pointer\"\n onClick={onExpand}\n />\n )}\n </div>\n\n {/* Bottom Content */}\n <div className=\"rpvs-flex rpvs-flex-col\">\n {contents && <div>{contents}</div>}\n {buttons && <div>{buttons}</div>}\n </div>\n </div>\n );\n};\n","import React, { useEffect, useRef, useState } from \"react\";\nimport { VideoPlayer } from \"./VideoPlayer\";\nimport { Product, VideoConfig } from \"../types\";\nimport { ArrowDown, ArrowUp, Close, Minimize } from \"../utils/icons\";\n\ninterface ExpandedProductModalProps {\n products: Product[];\n activeIndex: number;\n onClose: () => void;\n videoConfig?: Partial<VideoConfig>;\n contents?: (product: Product) => React.ReactNode;\n buttons?: (product: Product) => React.ReactNode;\n}\n\nexport const ProductModalY: React.FC<ExpandedProductModalProps> = ({\n products,\n activeIndex,\n onClose,\n videoConfig,\n contents,\n buttons,\n}) => {\n const [index, setIndex] = useState(activeIndex);\n const containerRef = useRef<HTMLDivElement>(null);\n const cardRefs = useRef<(HTMLDivElement | null)[]>([]);\n const [animating, setAnimating] = useState(false);\n\n useEffect(() => {\n document.body.classList.add(\"overflow-hidden\");\n return () => document.body.classList.remove(\"overflow-hidden\");\n }, []);\n\n useEffect(() => {\n if (containerRef.current && cardRefs.current[index]) {\n cardRefs.current[index]?.scrollIntoView({\n block: \"start\",\n });\n }\n }, [index]);\n\n useEffect(() => {\n if (containerRef.current && cardRefs.current[index]) {\n setAnimating(true);\n cardRefs.current[index]?.scrollIntoView({\n behavior: \"smooth\",\n block: \"start\",\n });\n const timer = setTimeout(() => setAnimating(false), 300);\n return () => clearTimeout(timer);\n }\n }, [index]);\n\n const touchStartY = useRef<number | null>(null);\n const handleTouchStart = (e: React.TouchEvent) => {\n touchStartY.current = e.touches[0].clientY;\n };\n const handleTouchEnd = (e: React.TouchEvent) => {\n if (touchStartY.current === null) return;\n const deltaY = e.changedTouches[0].clientY - touchStartY.current;\n if (deltaY > 50 && index > 0) setIndex(index - 1);\n else if (deltaY < -50 && index < products.length - 1) setIndex(index + 1);\n touchStartY.current = null;\n };\n\n return (\n <div\n className=\"rpvs-fixed rpvs-inset-0 rpvs-z-50 rpvs-bg-black/90 rpvs-flex rpvs-justify-center rpvs-items-center rpvs-transition-opacity\"\n onTouchStart={handleTouchStart}\n onTouchEnd={handleTouchEnd}\n role=\"dialog\"\n aria-modal=\"true\"\n >\n {/* Close button */}\n <button\n onClick={onClose}\n className=\"rpvs-absolute rpvs-top-4 rpvs-left-4 rpvs-z-50 rpvs-text-white rpvs-cursor-pointer\"\n aria-label=\"Close modal\"\n >\n <Minimize className=\"rpvs-w-6 rpvs-h-6 rpvs-text-white\" />\n {/* <Close className=\"rpvs-w-6 rpvs-h-6 rpvs-text-white\" /> */}\n </button>\n\n {/* Prev/Next buttons (vertical navigation) */}\n <div className=\"rpvs-hidden md:rpvs-flex rpvs-flex-col rpvs-absolute rpvs-right-4 rpvs-top-1/2 -rpvs-translate-y-1/2 rpvs-z-50 rpvs-space-y-3\">\n {index > 0 ? (\n <button\n onClick={() => setIndex(index - 1)}\n className=\"rpvs-p-3 rpvs-bg-white/60 rpvs-rounded-full hover:rpvs-bg-white/80 rpvs-cursor-pointer\"\n >\n <ArrowUp className=\"rpvs-w-6 rpvs-h-6 rpvs-text-black\" />\n </button>\n ) : (\n <button disabled className=\"rpvs-opacity-0\" />\n )}\n {index < products.length - 1 ? (\n <button\n onClick={() => setIndex(index + 1)}\n className=\"rpvs-p-3 rpvs-bg-white/60 rpvs-rounded-full hover:rpvs-bg-white/80 rpvs-cursor-pointer\"\n >\n <ArrowDown className=\"rpvs-w-6 rpvs-h-6 rpvs-text-black\" />\n </button>\n ) : (\n <button disabled className=\"rpvs-opacity-0\" />\n )}\n </div>\n\n {/* Scrollable container */}\n <div\n ref={containerRef}\n className=\"rpvs-w-full rpvs-h-full rpvs-overflow-y-auto rpvs-snap-y rpvs-snap-mandatory rpvs-scroll-smooth rpvs-hide-scrollbar\"\n >\n {products.map((product, i) => (\n <div\n key={product.id}\n ref={(el) => {\n cardRefs.current[i] = el;\n }}\n className=\"rpvs-w-full rpvs-min-h-screen rpvs-snap-start rpvs-flex rpvs-justify-center rpvs-items-center\"\n >\n <div\n className={`rpvs-w-full rpvs-max-w-lg md:rpvs-max-w-2xl rpvs-h-screen rpvs-max-h-screen rpvs-flex rpvs-flex-col rpvs-items-center rpvs-bg-black rpvs-text-white rpvs-overflow-hidden rpvs-rounded rpvs-shadow-lg rpvs-transition-all rpvs-duration-300 rpvs-ease-in-out \n ${\n animating && i === index\n ? \"rpvs-scale-95 rpvs-opacity-50\"\n : \"rpvs-scale-100 rpvs-opacity-100\"\n }`}\n >\n {/* Video */}\n <div className=\"rpvs-relative rpvs-flex-grow rpvs-w-full rpvs-flex rpvs-items-center rpvs-justify-center rpvs-bg-black\">\n <VideoPlayer\n videoUrl={product.videoUrl}\n videoConfig={videoConfig}\n />\n </div>\n\n {/* Contents */}\n <div className=\"rpvs-p-4 rpvs-space-y-3 rpvs-w-full rpvs-bg-black/30\">\n {contents && contents(product)}\n {buttons && buttons(product)}\n </div>\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n};\n","import React, { useEffect, useRef, useState } from \"react\";\nimport { VideoPlayer } from \"./VideoPlayer\";\nimport { Product, VideoConfig } from \"../types\";\nimport { ArrowLeft, ArrowRight, Close, Minimize } from \"../utils/icons\";\n\ninterface ExpandedProductModalProps {\n products: Product[];\n activeIndex: number;\n onClose: () => void;\n videoConfig?: Partial<VideoConfig>;\n contents?: (product: Product) => React.ReactNode;\n buttons?: (product: Product) => React.ReactNode;\n}\n\nexport const ProductModalX: React.FC<ExpandedProductModalProps> = ({\n products,\n activeIndex,\n onClose,\n videoConfig,\n contents,\n buttons,\n}) => {\n const [index, setIndex] = useState(activeIndex);\n const [animating, setAnimating] = useState(false);\n\n useEffect(() => {\n document.body.classList.add(\"overflow-hidden\");\n return () => document.body.classList.remove(\"overflow-hidden\");\n }, []);\n\n useEffect(() => {\n setAnimating(true);\n const timer = setTimeout(() => setAnimating(false), 300);\n return () => clearTimeout(timer);\n }, [index]);\n\n const touchStartX = useRef<number | null>(null);\n const handleTouchStart = (e: React.TouchEvent) => {\n touchStartX.current = e.touches[0].clientX;\n };\n const handleTouchEnd = (e: React.TouchEvent) => {\n if (touchStartX.current === null) return;\n const deltaX = e.changedTouches[0].clientX - touchStartX.current;\n if (deltaX > 50 && index > 0) setIndex(index - 1);\n else if (deltaX < -50 && index < products.length - 1) setIndex(index + 1);\n touchStartX.current = null;\n };\n\n const currentProduct = products[index];\n\n return (\n <div\n className=\"rpvs-fixed rpvs-inset-0 rpvs-z-50 rpvs-bg-black/90 rpvs-flex rpvs-justify-center rpvs-items-center rpvs-transition-opacity\"\n onTouchStart={handleTouchStart}\n onTouchEnd={handleTouchEnd}\n >\n {/* Close button */}\n <button\n onClick={onClose}\n className=\"rpvs-absolute rpvs-top-4 rpvs-left-4 rpvs-z-50 rpvs-p-3 rpvs-cursor-pointer\"\n aria-label=\"Close modal\"\n >\n <Minimize className=\"rpvs-w-6 rpvs-h-6 rpvs-text-white\" />\n {/* <Close className=\"rpvs-w-6 rpvs-h-6 rpvs-text-white\" /> */}\n </button>\n\n {/* Prev/Next buttons - md+ only */}\n <div className=\"rpvs-hidden md:rpvs-flex rpvs-flex-row rpvs-absolute rpvs-top-1/2 -rpvs-translate-y-1/2 rpvs-z-50 rpvs-justify-between rpvs-w-full rpvs-px-4\">\n {index > 0 ? (\n <button\n onClick={() => setIndex(index - 1)}\n className=\"rpvs-p-3 rpvs-bg-white/60 rpvs-rounded-full hover:rpvs-bg-white/80 rpvs-cursor-pointer\"\n >\n <ArrowLeft className=\"rpvs-w-6 rpvs-h-6 rpvs-text-black\" />\n </button>\n ) : (\n <div />\n )}\n\n {index < products.length - 1 ? (\n <button\n onClick={() => setIndex(index + 1)}\n className=\"rpvs-p-3 rpvs-bg-white/60 rpvs-rounded-full hover:rpvs-bg-white/80 rpvs-cursor-pointer\"\n >\n <ArrowRight className=\"rpvs-w-6 rpvs-h-6 rpvs-text-black\" />\n </button>\n ) : (\n <div />\n )}\n </div>\n\n {/* Active Product Card */}\n <div className=\"rpvs-w-full rpvs-h-full rpvs-flex rpvs-justify-center rpvs-items-center rpvs-overflow-hidden\">\n <div\n className={`rpvs-w-full rpvs-max-w-lg md:rpvs-max-w-2xl rpvs-h-screen rpvs-max-h-screen rpvs-flex rpvs-flex-col rpvs-items-center rpvs-bg-black rpvs-text-white rpvs-overflow-hidden rpvs-rounded rpvs-shadow-lg rpvs-transition-all rpvs-duration-300 rpvs-ease-in-out \n ${\n animating\n ? \"rpvs-scale-95 rpvs-opacity-50\"\n : \"rpvs-scale-100 rpvs-opacity-100\"\n }`}\n >\n {/* Video */}\n <div className=\"rpvs-relative rpvs-flex-grow rpvs-w-full rpvs-flex rpvs-items-center rpvs-justify-center rpvs-bg-black\">\n <VideoPlayer\n videoUrl={currentProduct.videoUrl}\n videoConfig={videoConfig}\n />\n </div>\n\n {/* Contents */}\n <div className=\"rpvs-p-4 rpvs-space-y-3 rpvs-w-full rpvs-bg-black/30\">\n {contents && contents(currentProduct)}\n {buttons && buttons(currentProduct)}\n </div>\n </div>\n </div>\n </div>\n );\n};\n","import { useEffect, useState } from \"react\";\n\nexport function useDeviceType() {\n const [device, setDevice] = useState<\"mobile\" | \"tablet\" | \"desktop\">(\n \"desktop\"\n );\n\n useEffect(() => {\n const updateDevice = () => {\n const width = window.innerWidth;\n if (width <= 640) setDevice(\"mobile\");\n else if (width <= 1024) setDevice(\"tablet\");\n else setDevice(\"desktop\");\n };\n\n updateDevice();\n window.addEventListener(\"resize\", updateDevice);\n return () => window.removeEventListener(\"resize\", updateDevice);\n }, []);\n\n return device;\n}\n","\n// AUTO-GENERATED FILE\nlet injected = false;\nexport function injectStyles() {\n if (injected || typeof document === \"undefined\") return;\n injected = true;\n const style = document.createElement(\"style\");\n style.textContent = `*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:\"\"}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}.rpvs-fixed{position:fixed}.rpvs-absolute{position:absolute}.rpvs-relative{position:relative}.rpvs-inset-0{inset:0}.rpvs-left-4{left:1rem}.rpvs-right-0{right:0}.rpvs-right-4{right:1rem}.rpvs-top-0{top:0}.rpvs-top-1{top:.25rem}.rpvs-top-1\\\\/2{top:50%}.rpvs-top-4{top:1rem}.rpvs-z-10{z-index:10}.rpvs-z-50{z-index:50}.rpvs-my-1{margin-bottom:.25rem;margin-top:.25rem}.rpvs-my-1\\\\.5{margin-bottom:.375rem;margin-top:.375rem}.rpvs-my-4{margin-top:1rem}.rpvs-mb-4,.rpvs-my-4{margin-bottom:1rem}.rpvs-ml-2{margin-left:.5rem}.rpvs-block{display:block}.rpvs-flex{display:flex}.rpvs-grid{display:grid}.rpvs-hidden{display:none}.rpvs-aspect-\\\\[4\\\\/5\\\\]{aspect-ratio:4/5}.rpvs-aspect-video{aspect-ratio:16/9}.rpvs-h-12{height:3rem}.rpvs-h-5{height:1.25rem}.rpvs-h-6{height:1.5rem}.rpvs-h-8{height:2rem}.rpvs-h-full{height:100%}.rpvs-h-screen{height:100vh}.rpvs-max-h-screen{max-height:100vh}.rpvs-min-h-\\\\[50vh\\\\]{min-height:50vh}.rpvs-min-h-screen{min-height:100vh}.rpvs-w-12{width:3rem}.rpvs-w-6{width:1.5rem}.rpvs-w-8{width:2rem}.rpvs-w-\\\\[30\\\\%\\\\]{width:30%}.rpvs-w-full{width:100%}.rpvs-min-w-\\\\[250px\\\\]{min-width:250px}.rpvs-min-w-full{min-width:100%}.rpvs-max-w-lg{max-width:32rem}.rpvs-max-w-sm{max-width:24rem}.rpvs-flex-shrink-0{flex-shrink:0}.rpvs-flex-grow{flex-grow:1}.-rpvs-translate-y-1{--tw-translate-y:-0.25rem}.-rpvs-translate-y-1,.-rpvs-translate-y-1\\\\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-rpvs-translate-y-1\\\\/2{--tw-translate-y:-50%}.rpvs-scale-100{--tw-scale-x:1;--tw-scale-y:1}.rpvs-scale-100,.rpvs-scale-95{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rpvs-scale-95{--tw-scale-x:.95;--tw-scale-y:.95}@keyframes rpvs-spin{to{transform:rotate(1turn)}}.rpvs-animate-spin{animation:rpvs-spin 1s linear infinite}.rpvs-cursor-pointer{cursor:pointer}.rpvs-snap-x{scroll-snap-type:x var(--tw-scroll-snap-strictness)}.rpvs-snap-y{scroll-snap-type:y var(--tw-scroll-snap-strictness)}.rpvs-snap-mandatory{--tw-scroll-snap-strictness:mandatory}.rpvs-snap-start{scroll-snap-align:start}.rpvs-flex-row{flex-direction:row}.rpvs-flex-col{flex-direction:column}.rpvs-items-center{align-items:center}.rpvs-justify-center{justify-content:center}.rpvs-justify-between{justify-content:space-between}.rpvs-gap-1{gap:.25rem}.rpvs-gap-2{gap:.5rem}.rpvs-gap-3{gap:.75rem}.rpvs-gap-4{gap:1rem}.rpvs-gap-5{gap:1.25rem}.rpvs-space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.rpvs-overflow-hidden{overflow:hidden}.rpvs-overflow-x-auto{overflow-x:auto}.rpvs-overflow-y-auto{overflow-y:auto}.rpvs-scroll-smooth{scroll-behavior:smooth}.rpvs-rounded{border-radius:.25rem}.rpvs-rounded-full{border-radius:9999px}.rpvs-border-4{border-width:4px}.rpvs-border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity,1))}.rpvs-border-t-transparent{border-top-color:#0000}.rpvs-bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity,1))}.rpvs-bg-black\\\\/30{background-color:#0000004d}.rpvs-bg-black\\\\/90{background-color:#000000e6}.rpvs-bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity,1))}.rpvs-bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.rpvs-bg-transparent{background-color:initial}.rpvs-bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.rpvs-bg-white\\\\/60{background-color:#fff9}.rpvs-p-1{padding:.25rem}.rpvs-p-2{padding:.5rem}.rpvs-p-3{padding:.75rem}.rpvs-p-4{padding:1rem}.rpvs-p-5{padding:1.25rem}.rpvs-px-2{padding-left:.5rem;padding-right:.5rem}.rpvs-px-4{padding-left:1rem;padding-right:1rem}.rpvs-py-2{padding-bottom:.5rem;padding-top:.5rem}.rpvs-text-center{text-align:center}.rpvs-text-2xl{font-size:1.5rem;line-height:2rem}.rpvs-text-xl{font-size:1.25rem;line-height:1.75rem}.rpvs-font-bold{font-weight:700}.rpvs-font-semibold{font-weight:600}.rpvs-uppercase{text-transform:uppercase}.rpvs-text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.rpvs-text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.rpvs-text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.rpvs-text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.rpvs-text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.rpvs-underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.rpvs-line-through{-webkit-text-decoration-line:line-through;text-decoration-line:line-through}.rpvs-opacity-0{opacity:0}.rpvs-opacity-100{opacity:1}.rpvs-opacity-50{opacity:.5}.rpvs-shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.rpvs-transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.rpvs-transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.rpvs-duration-300{transition-duration:.3s}.rpvs-ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.rpvs-hide-scrollbar::-webkit-scrollbar{display:none}.rpvs-hide-scrollbar{-ms-overflow-style:none;scrollbar-width:none}.hover\\\\:rpvs-bg-white\\\\/80:hover{background-color:#fffc}@media (min-width:768px){.md\\\\:rpvs-flex{display:flex}.md\\\\:rpvs-aspect-\\\\[5\\\\/6\\\\]{aspect-ratio:5/6}.md\\\\:rpvs-max-w-2xl{max-width:42rem}}`;\n document.head.appendChild(style);\n}\n"],"mappings":"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