1+ "use client" ;
2+
3+ import { useEffect , useState } from "react" ;
4+ import { cn } from "@/lib/utils" ;
5+ import { Check } from "lucide-react" ;
6+
7+ // Section configuration - maps to each wrapper component
8+ const SECTIONS = [
9+ { id : "about-section" , label : "About Us" } ,
10+ { id : "software" , label : "Software Development" } ,
11+ { id : "expect" , label : "What to Expect" } ,
12+ { id : "roadmap" , label : "Your Roadmap" } ,
13+ { id : "house-rules" , label : "House Rules" } ,
14+ { id : "team" , label : "Meet the Team" } ,
15+ { id : "partners" , label : "Our Partners" } ,
16+ { id : "welcome" , label : "Welcome Aboard" } ,
17+ ] ;
18+
19+ export default function OnboardingStepper ( ) {
20+ const [ activeSection , setActiveSection ] = useState < string > ( SECTIONS [ 0 ] ?. id || "about-section" ) ;
21+
22+ useEffect ( ( ) => {
23+ // Intersection Observer to track which section is in view
24+ const observerOptions = {
25+ root : null ,
26+ rootMargin : "-50% 0px -50% 0px" , // Trigger when section crosses middle of viewport
27+ threshold : 0 ,
28+ } ;
29+
30+ const observerCallback = ( entries : IntersectionObserverEntry [ ] ) => {
31+ entries . forEach ( ( entry ) => {
32+ if ( entry . isIntersecting ) {
33+ setActiveSection ( entry . target . id ) ;
34+ }
35+ } ) ;
36+ } ;
37+
38+ const observer = new IntersectionObserver (
39+ observerCallback ,
40+ observerOptions
41+ ) ;
42+
43+ // Observe each section
44+ SECTIONS . forEach ( ( { id } ) => {
45+ const element = document . getElementById ( id ) ;
46+ if ( element ) observer . observe ( element ) ;
47+ } ) ;
48+
49+ return ( ) => observer . disconnect ( ) ;
50+ } , [ ] ) ;
51+
52+ // Scroll to section when circle is clicked
53+ const scrollToSection = ( sectionId : string ) => {
54+ const element = document . getElementById ( sectionId ) ;
55+ if ( element ) {
56+ element . scrollIntoView ( { behavior : "smooth" , block : "center" } ) ;
57+ }
58+ } ;
59+
60+ return (
61+ < nav
62+ className = "fixed left-6 top-1/2 z-50 hidden -translate-y-1/2 lg:block"
63+ aria-label = "Onboarding progress"
64+ >
65+ < div className = "relative flex flex-col" >
66+ { /* Connecting line background */ }
67+ < div className = "absolute left-2 top-2 h-[calc(100%-16px)] w-0.5 bg-gradient-to-b from-gray-300 to-gray-200 dark:from-gray-700 dark:to-gray-800" />
68+
69+ { /* Progress line that fills as you scroll */ }
70+ < div
71+ className = "absolute left-2 top-2 w-0.5 bg-gradient-to-b from-cyan-500 via-blue-500 to-purple-600 transition-all duration-500 ease-out"
72+ style = { {
73+ height : `${ ( SECTIONS . findIndex ( ( s ) => s . id === activeSection ) / ( SECTIONS . length - 1 ) ) * 100 } %`
74+ } }
75+ />
76+
77+ { SECTIONS . map ( ( section , index ) => {
78+ const isActive = activeSection === section . id ;
79+ const isPassed = SECTIONS . findIndex ( ( s ) => s . id === activeSection ) > index ;
80+ const isLast = index === SECTIONS . length - 1 ;
81+
82+ return (
83+ < div
84+ key = { section . id }
85+ className = { cn (
86+ "relative z-10" ,
87+ ! isLast && "mb-8"
88+ ) }
89+ >
90+ < button
91+ onClick = { ( ) => scrollToSection ( section . id ) }
92+ className = "group relative flex items-center"
93+ aria-label = { `Go to ${ section . label } ` }
94+ aria-current = { isActive ? "step" : undefined }
95+ >
96+ { /* Circle indicator */ }
97+ < div
98+ className = { cn (
99+ "relative flex h-5 w-5 items-center justify-center rounded-full border-2 transition-all duration-300" ,
100+ "hover:scale-110 focus-visible:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2" ,
101+ isActive && "scale-125 border-transparent bg-gradient-to-br from-cyan-500 via-blue-600 to-purple-600 shadow-lg shadow-blue-500/50" ,
102+ isPassed && ! isActive && "border-blue-600 bg-blue-600 shadow-md shadow-blue-500/30" ,
103+ ! isActive && ! isPassed && "border-gray-400 bg-white dark:border-gray-600 dark:bg-gray-900"
104+ ) }
105+ >
106+ { /* Checkmark for passed sections */ }
107+ { isPassed && ! isActive && (
108+ < Check className = "h-3 w-3 text-white" strokeWidth = { 3 } />
109+ ) }
110+
111+ { /* Pulsing ring for active section */ }
112+ { isActive && (
113+ < >
114+ < div className = "absolute inset-0 rounded-full bg-gradient-to-br from-cyan-500 via-blue-600 to-purple-600 animate-ping opacity-75" />
115+ < div className = "absolute inset-0 rounded-full bg-white dark:bg-gray-900" style = { { transform : "scale(0.4)" } } />
116+ </ >
117+ ) }
118+ </ div >
119+
120+ { /* Enhanced tooltip label on hover */ }
121+ < div className = "pointer-events-none absolute left-8 flex items-center gap-2" >
122+ < div className = "h-px w-3 bg-gradient-to-r from-gray-400 to-transparent opacity-0 transition-all duration-200 group-hover:w-4 group-hover:opacity-100 dark:from-gray-600" />
123+ < span
124+ className = { cn (
125+ "whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium shadow-xl backdrop-blur-sm transition-all duration-200" ,
126+ "border border-gray-200 bg-white/95 text-gray-900 dark:border-gray-700 dark:bg-gray-800/95 dark:text-white" ,
127+ "opacity-0 -translate-x-2 group-hover:translate-x-0 group-hover:opacity-100" ,
128+ isActive && "border-blue-500/20 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-950/50 dark:to-purple-950/50"
129+ ) }
130+ >
131+ { section . label }
132+ { isPassed && ! isActive && (
133+ < span className = "ml-2 text-xs text-blue-600 dark:text-blue-400" > ✓</ span >
134+ ) }
135+ </ span >
136+ </ div >
137+ </ button >
138+ </ div >
139+ ) ;
140+ } ) }
141+ </ div >
142+ </ nav >
143+ ) ;
144+ }
0 commit comments