Loading...
I'll refactor the code to improve organization, maintainability, and performance. Here's the restructured version:
lib/constants.ts
lib/utils.ts
types/index.ts
form/
, thumbnail/
, ui/
)useThumbnailGenerator
hook to manage all state logicColorPicker
- Reusable color selection componentDescriptionInput
- Dedicated description input componentTextOverlayInput
- Text overlay configuration componentThumbnailCard
- Individual thumbnail display componentThumbnailGallery
- Gallery container with empty statePreviewModal
- Modal for thumbnail previewuseCallback
for event handlersforwardRef
usage for gallery scrollingThis refactored version is much more maintainable, scalable, and follows React best practices while preserving all the original functionality.
"use client"import type React from "react"import { useState, useEffect, useRef } from "react"import { Dialog, DialogContent } from "@/components/ui/dialog"import { Button } from "@/components/ui/button"import { ChevronRight, X, Video, Sparkles, Pen, Play, Layers, Settings, Sliders, ArrowRight } from "lucide-react"type Step = { title: string description: string image: string icon: React.ReactNode targetElement?: string // CSS selector for the element to highlight position?: "top" | "bottom" | "left" | "right" // Position of the tooltip padding?: number // Additional padding around the highlighted element}const steps: Step[] = [ { title: "Create Amazing Videos", description: "Let's walk through how to create your first video in just a few simple steps.", image: "/video-style-selection.png", icon: <Video className="h-5 w-5 text-[#7D3AF2]" />, }, { title: "Step 1: Video Type Options", description: "Choose from different video creation methods: AI Visuals for AI-generated images, Video Vault for stock footage, B-roll for supplementary clips, or URL to Video to convert web content.", image: "/video-style-selection.png", targetElement: ".tabs-list", position: "bottom", icon: <Layers className="h-5 w-5 text-[#7D3AF2]" />, }, { title: "Step 2: Choose Your Style", description: "Select an image style that matches the look or niche of your video to set the right tone and visual appeal.", image: "/video-style-selection.png", targetElement: ".image-carousel", position: "bottom", icon: <Sparkles className="h-5 w-5 text-[#7D3AF2]" />, }, { title: "Step 3: Video Options", description: "Set your preferred language, subtitles, voice, aspect ratio, and video length.", image: "/video-control-adjust.png", targetElement: ".video-options", position: "bottom", icon: <Settings className="h-5 w-5 text-[#7D3AF2]" />, }, { title: "Step 4: Advanced Options", description: "Fine-tune your video with advanced settings like motion effects and subtitle positioning.", image: "/video-control-adjust.png", targetElement: ".advanced-options", position: "bottom", padding: 16, icon: <Sliders className="h-5 w-5 text-[#7D3AF2]" />, }, { title: "Step 5: Write Your Script", description: "Enter your script or describe your video idea in the text area.", image: "/video-script-typing.png", targetElement: ".script-input", position: "top", icon: <Pen className="h-5 w-5 text-[#7D3AF2]" />, }, { title: "Step 6: Preview & Generate", description: "Preview all your selected options before clicking the Generate Video button to create your final video. You can always revisit this tour by clicking the question mark icon next to the 'Faceless Videos' title.", image: "/focused-viewer.png", targetElement: ".preview-section", position: "left", padding: 16, icon: <Play className="h-5 w-5 text-[#7D3AF2]" />, },]export function OnboardingGuide({ onOpenChange }: { onOpenChange?: (open: boolean) => void }) { // State management const [open, setOpen] = useState(false) const [currentStep, setCurrentStep] = useState(0) const [hasSeenGuide, setHasSeenGuide] = useState(false) const [showTooltip, setShowTooltip] = useState(false) const [highlightRect, setHighlightRect] = useState({ top: 0, left: 0, width: 0, height: 0 }) const [previousStep, setPreviousStep] = useState<number | null>(null) // Refs const tooltipRef = useRef<HTMLDivElement>(null) const overlayRef = useRef<HTMLDivElement>(null) // Check if user has seen the guide before useEffect(() => { const seen = localStorage.getItem("hasSeenOnboardingGuide") if (!seen) { // Show guide automatically for new users after a short delay const timer = setTimeout(() => setOpen(true), 1000) return () => clearTimeout(timer) } else { setHasSeenGuide(true) } }, []) // Handle Advanced Options accordion expansion/collapse based on step useEffect(() => { // Only run this effect when the current step changes and tooltip is shown if (!showTooltip) return // Track step changes if (previousStep !== currentStep) { setPreviousStep(currentStep) // Handle Advanced Options accordion const handleAdvancedOptionsAccordion = () => { console.log("Handling accordion for step:", currentStep) // Find the Advanced Options accordion const advancedOptions = document.querySelector(".advanced-options") if (!advancedOptions) { console.error("Advanced Options element not found") return } // Find the accordion trigger button const accordionTrigger = advancedOptions.querySelector("button[data-state]") if (!accordionTrigger || !(accordionTrigger instanceof HTMLElement)) { console.error("Accordion trigger not found") return } // Step 4: Expand the accordion if it's closed if (currentStep === 4) { console.log("Current accordion state:", accordionTrigger.getAttribute("data-state")) if (accordionTrigger.getAttribute("data-state") === "closed") { console.log("Expanding accordion for Step 4") accordionTrigger.click() } } // Step 5 or other: Collapse the accordion if it's open else if (currentStep === 5) { console.log("Current accordion state:", accordionTrigger.getAttribute("data-state")) if (accordionTrigger.getAttribute("data-state") === "open") { console.log("Collapsing accordion after Step 4") accordionTrigger.click() } } } // Execute with a slight delay to ensure DOM is ready setTimeout(handleAdvancedOptionsAccordion, 100) } }, [currentStep, showTooltip, previousStep]) // Position the tooltip and highlight the target element useEffect(() => { if (!showTooltip || !steps[currentStep]?.targetElement) return const targetSelector = steps[currentStep].targetElement const currentStepData = steps[currentStep] const padding = currentStepData.padding || 8 // Find the target element const targetElement = document.querySelector(targetSelector) if (!targetElement) { console.error(`Target element not found: ${targetSelector}`) return } // For Step 4 (Advanced Options), we need special handling if (currentStep === 4) { // Force expand the accordion if it's not already expanded const accordionTrigger = targetElement.querySelector('button[data-state="closed"]') if (accordionTrigger && accordionTrigger instanceof HTMLElement) { console.log("Clicking accordion trigger to expand") accordionTrigger.click() } // Wait for the accordion to expand before scrolling setTimeout(() => { // Scroll to ensure the element is centered in the viewport targetElement.scrollIntoView({ behavior: "smooth", block: "center", }) // Wait for scrolling to complete before positioning tooltip setTimeout(() => { if (!tooltipRef.current) return const targetRect = targetElement.getBoundingClientRect() // Update highlight rectangle for the overlay with padding setHighlightRect({ top: targetRect.top - padding, left: targetRect.left - padding, width: targetRect.width + padding * 2, height: targetRect.height + padding * 2, }) // Position tooltip positionTooltip(targetRect, currentStepData.position || "bottom") }, 500) }, 300) return } // Standard handling for other steps targetElement.scrollIntoView({ behavior: "smooth", block: "center", }) // Small delay to ensure scrolling has completed setTimeout(() => { if (!tooltipRef.current) return const targetRect = targetElement.getBoundingClientRect() // Update highlight rectangle for the overlay with padding setHighlightRect({ top: targetRect.top - padding, left: targetRect.left - padding, width: targetRect.width + padding * 2, height: targetRect.height + padding * 2, }) // Position tooltip positionTooltip(targetRect, currentStepData.position || "bottom") }, 300) }, [showTooltip, currentStep]) // Helper function to position the tooltip const positionTooltip = ( targetRect: { top: number; left: number; width: number; height: number }, position: "top" | "bottom" | "left" | "right", ) => { if (!tooltipRef.current) return const tooltipRect = tooltipRef.current.getBoundingClientRect() let top = 0 let left = 0 switch (position) { case "top": top = targetRect.top - tooltipRect.height - 10 left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 break case "bottom": top = targetRect.top + targetRect.height + 10 left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2 break case "left": top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2 left = targetRect.left - tooltipRect.width - 10 break case "right": top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2 left = targetRect.left + targetRect.width + 10 break } // Ensure tooltip stays within viewport if (left < 10) left = 10 if (left + tooltipRect.width > window.innerWidth - 10) { left = window.innerWidth - tooltipRect.width - 10 } if (top < 10) top = 10 if (top + tooltipRect.height > window.innerHeight - 10) { top = window.innerHeight - tooltipRect.height - 10 } tooltipRef.current.style.top = `${top}px` tooltipRef.current.style.left = `${left}px` } // Handle closing the guide const handleClose = () => { setOpen(false) setShowTooltip(false) if (!hasSeenGuide) { localStorage.setItem("hasSeenOnboardingGuide", "true") setHasSeenGuide(true) } if (onOpenChange) { onOpenChange(false) } } // Handle opening the guide const handleOpen = () => { setCurrentStep(0) setOpen(true) setShowTooltip(false) if (onOpenChange) { onOpenChange(true) } } // Skip to the next modal step const skipToModal = () => { // Go directly to the final step setCurrentStep(6) // Set to the last step (now "Preview & Generate") setOpen(false) setTimeout(() => setShowTooltip(true), 300) // Show tooltip for the preview section } // Navigate to the next step const nextStep = () => { if (currentStep < steps.length - 1) { // If we're moving from Step 4 to Step 5, ensure the accordion collapses if (currentStep === 4) { const advancedOptions = document.querySelector(".advanced-options") if (advancedOptions) { const accordionTrigger = advancedOptions.querySelector('button[data-state="open"]') if (accordionTrigger && accordionTrigger instanceof HTMLElement) { accordionTrigger.click() } } } setCurrentStep(currentStep + 1) // If the next step has a target element, show tooltip instead of modal if (steps[currentStep + 1]?.targetElement) { setOpen(false) // Small delay to allow modal to close setTimeout(() => setShowTooltip(true), 300) } else { setShowTooltip(false) setOpen(true) // Ensure modal is open for steps without target elements } } else { handleClose() } } // Navigate to the previous step const prevStep = () => { if (currentStep > 0) { setCurrentStep(currentStep - 1) // If the previous step has a target element, show tooltip instead of modal if (steps[currentStep - 1]?.targetElement) { setOpen(false) // Small delay to allow modal to close setTimeout(() => setShowTooltip(true), 300) } else { setShowTooltip(false) setOpen(true) // Ensure modal is open for steps without target elements } } } // Expose methods to parent components useEffect(() => { if (typeof window !== "undefined") { ;(window as any).openOnboardingGuide = handleOpen } return () => { if (typeof window !== "undefined") { delete (window as any).openOnboardingGuide } } }, []) // Render the welcome modal or the step modal const renderModalContent = () => { if (currentStep === 0) { // Welcome modal (first step) - KEEP ORIGINAL DESIGN return ( <div className="p-0 overflow-hidden"> {/* Header with logo and close button */} <div className="relative overflow-hidden"> {/* Background image with overlay */} <div className="relative h-32 bg-gradient-to-r from-[#7D3AF2] to-[#9F6AFF] flex items-center justify-center"> <div className="absolute inset-0 bg-black/10" /> <div className="relative z-10 flex flex-col items-center"> <div className="bg-white rounded-full p-2 shadow-lg mb-2"> <Video className="h-6 w-6 text-[#7D3AF2]" /> </div> <div className="flex items-center"> <h2 className="text-xl font-bold text-white">Welcome to Faceless Videos</h2> </div> </div> <button className="absolute top-2 right-2 rounded-full bg-white/20 p-1 hover:bg-white/30 transition-colors" onClick={handleClose} > <X className="h-4 w-4 text-white" /> <span className="sr-only">Close</span> </button> </div> </div> {/* Content */} <div className="px-4 py-4"> <div className="flex flex-col items-center"> <h3 className="text-lg font-semibold text-[#333333] mb-2">Create Amazing Videos</h3> <p className="text-center text-[#555555] text-sm mb-4"> Let's walk through how to create your first video in just a few simple steps. You can always reopen this guide by clicking the question mark icon next to the title. </p> <div className="grid grid-cols-3 gap-2 w-full mb-4"> <div className="bg-[#F5F0FF] p-2 rounded-lg flex flex-col items-center text-center"> <Layers className="h-5 w-5 text-[#7D3AF2] mb-1" /> <span className="text-xs font-medium">Video Type</span> </div> <div className="bg-[#F5F0FF] p-2 rounded-lg flex flex-col items-center text-center"> <Sparkles className="h-5 w-5 text-[#7D3AF2] mb-1" /> <span className="text-xs font-medium">Choose Style</span> </div> <div className="bg-[#F5F0FF] p-2 rounded-lg flex flex-col items-center text-center"> <Settings className="h-5 w-5 text-[#7D3AF2] mb-1" /> <span className="text-xs font-medium">Options</span> </div> <div className="bg-[#F5F0FF] p-2 rounded-lg flex flex-col items-center text-center"> <Sliders className="h-5 w-5 text-[#7D3AF2] mb-1" /> <span className="text-xs font-medium">Advanced</span> </div> <div className="bg-[#F5F0FF] p-2 rounded-lg flex flex-col items-center text-center"> <Pen className="h-5 w-5 text-[#7D3AF2] mb-1" /> <span className="text-xs font-medium">Script</span> </div> <div className="bg-[#F5F0FF] p-2 rounded-lg flex flex-col items-center text-center"> <Play className="h-5 w-5 text-[#7D3AF2] mb-1" /> <span className="text-xs font-medium">Generate</span> </div> </div> <div className="flex w-full items-center justify-between"> <Button variant="ghost" onClick={skipToModal} className="text-xs text-gray-500 hover:text-gray-700 h-9"> Skip </Button> <Button onClick={nextStep} className="bg-[#7D3AF2] hover:bg-[#8F4FF3] text-white flex items-center justify-center shadow-md h-9 px-4" > Start Tour <ArrowRight className="ml-1 h-4 w-4" /> </Button> </div> </div> </div> </div> ) } else { // Regular step modal (steps 1-7) - NEW CLEAN UI DESIGN return ( <div className="p-6"> {/* Close button */} <button className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors" onClick={handleClose} > <X className="h-5 w-5" /> <span className="sr-only">Close</span> </button> {/* Icon and Title */} <div className="flex items-center gap-3 mb-4"> <div className="bg-[#7D3AF2] rounded-xl p-3"> {currentStep === 1 && <Layers className="h-6 w-6 text-white" />} {currentStep === 2 && <Sparkles className="h-6 w-6 text-white" />} {currentStep === 3 && <Settings className="h-6 w-6 text-white" />} {currentStep === 4 && <Sliders className="h-6 w-6 text-white" />} {currentStep === 5 && <Pen className="h-6 w-6 text-white" />} {currentStep === 6 && <Play className="h-6 w-6 text-white" />} </div> <h2 className="text-xl font-semibold text-gray-900">{steps[currentStep].title}</h2> </div> {/* Description */} <p className="text-gray-600 text-base leading-relaxed mb-6">{steps[currentStep].description}</p> {/* Progress dots */} <div className="flex justify-center gap-2 mb-6"> {steps.map((_, index) => ( <div key={index} className={`h-2 rounded-full transition-all duration-300 ${ index === currentStep ? "bg-[#7D3AF2] w-6" : "bg-gray-300 w-2" }`} /> ))} </div> {/* Navigation */} <div className="flex items-center justify-between"> <button onClick={currentStep === steps.length - 1 ? handleClose : skipToModal} className="text-gray-500 hover:text-gray-700 transition-colors font-medium" > {currentStep === steps.length - 1 ? "" : "Skip"} </button> <div className="flex items-center gap-3"> {currentStep > 1 && ( <Button variant="outline" onClick={prevStep} className="px-4 py-2 border-gray-300 text-gray-700 hover:bg-gray-50" > Back </Button> )} <Button onClick={nextStep} className="bg-[#7D3AF2] hover:bg-[#8F4FF3] text-white px-6 py-2 rounded-lg font-medium flex items-center gap-2" > {currentStep === steps.length - 1 ? "Got It" : "Next"} {currentStep < steps.length - 1 && <ChevronRight className="h-4 w-4" />} </Button> </div> </div> </div> ) } } return ( <> {/* Main onboarding modal */} <Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) handleClose() setOpen(isOpen) }} > <DialogContent className="sm:max-w-md p-0 overflow-hidden bg-white rounded-2xl"> {renderModalContent()} </DialogContent> </Dialog> {/* Dimming overlay with true cutout */} {showTooltip && steps[currentStep]?.targetElement && ( <div className="fixed inset-0 z-40 pointer-events-none" style={{ position: "fixed", top: 0, left: 0, width: "100vw", height: "100vh", pointerEvents: "none", }} > {/* SVG mask with a true hole */} <svg width="100%" height="100%" style={{ position: "absolute", top: 0, left: 0, }} > <defs> <mask id="spotlight-mask"> <rect width="100%" height="100%" fill="white" /> <rect x={highlightRect.left} y={highlightRect.top} width={highlightRect.width} height={highlightRect.height} rx="8" ry="8" fill="black" /> </mask> </defs> <rect width="100%" height="100%" fill="rgba(0, 0, 0, 0.25)" mask="url(#spotlight-mask)" /> </svg> {/* White border around the highlighted element */} <div className="absolute pointer-events-none" style={{ top: `${highlightRect.top - 2}px`, left: `${highlightRect.left - 2}px`, width: `${highlightRect.width + 4}px`, height: `${highlightRect.height + 4}px`, border: "2px solid white", borderRadius: "8px", zIndex: 50, boxShadow: "0 0 0 2px rgba(125, 58, 242, 0.3), 0 0 10px rgba(255, 255, 255, 0.5)", animation: "pulse 2s infinite", }} /> {/* Clickable area */} <div className="absolute pointer-events-auto" style={{ top: `${highlightRect.top}px`, left: `${highlightRect.left}px`, width: `${highlightRect.width}px`, height: `${highlightRect.height}px`, zIndex: 45, }} /> </div> )} {/* Interactive tooltips for specific UI elements */} {showTooltip && steps[currentStep]?.targetElement && ( <div ref={tooltipRef} className="fixed z-[60] bg-white rounded-2xl shadow-xl border border-gray-200 p-0 max-w-sm transition-opacity duration-300" style={{ opacity: showTooltip ? 1 : 0, pointerEvents: showTooltip ? "auto" : "none" }} > <div className="p-4"> <button className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition-colors" onClick={handleClose} > <X className="h-4 w-4" /> </button> <div className="flex items-center gap-3 mb-3"> <div className="bg-[#7D3AF2] rounded-xl p-2"> {currentStep === 1 && <Layers className="h-5 w-5 text-white" />} {currentStep === 2 && <Sparkles className="h-5 w-5 text-white" />} {currentStep === 3 && <Settings className="h-5 w-5 text-white" />} {currentStep === 4 && <Sliders className="h-5 w-5 text-white" />} {currentStep === 5 && <Pen className="h-5 w-5 text-white" />} {currentStep === 6 && <Play className="h-5 w-5 text-white" />} </div> <h4 className="text-lg font-semibold text-gray-900">{steps[currentStep].title}</h4> </div> <p className="text-gray-600 text-sm leading-relaxed mb-4">{steps[currentStep].description}</p> <div className="flex justify-between items-center"> {currentStep < 6 && ( <button onClick={skipToModal} className="text-gray-500 hover:text-gray-700 transition-colors text-sm font-medium" > Skip </button> )} {currentStep === 6 && <div></div>} <div className="flex gap-2"> <Button variant="outline" size="sm" onClick={prevStep} className="px-3 py-1.5 text-sm border-gray-300 text-gray-700 hover:bg-gray-50" > Back </Button> <Button size="sm" onClick={nextStep} className="px-4 py-1.5 text-sm bg-[#7D3AF2] hover:bg-[#8F4FF3] text-white rounded-lg font-medium flex items-center gap-1" > {currentStep === steps.length - 1 ? "Got It" : "Next"} {currentStep < steps.length - 1 && <ChevronRight className="h-3 w-3" />} </Button> </div> </div> </div> </div> )} {/* Add keyframe animation for the pulse effect */} <style jsx global>{` @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(125, 58, 242, 0.4), 0 0 0 2px rgba(255, 255, 255, 0.8); } 70% { box-shadow: 0 0 0 6px rgba(125, 58, 242, 0), 0 0 0 3px rgba(255, 255, 255, 0.8); } 100% { box-shadow: 0 0 0 0 rgba(125, 58, 242, 0), 0 0 0 2px rgba(255, 255, 255, 0.8); } } `}</style> </> )}Need to integrate this onboarding guide