1+ "use client" ;
2+
3+ import { useEffect , useState } from "react" ;
4+ import { cn } from "@/lib/utils" ;
5+
6+ // Section configuration - maps to each wrapper component
7+ const SECTIONS = [
8+ { id : "software" , label : "Software Development" } ,
9+ { id : "expect" , label : "What to Expect" } ,
10+ { id : "roadmap" , label : "Your Roadmap" } ,
11+ { id : "house-rules" , label : "House Rules" } ,
12+ { id : "team" , label : "Meet the Team" } ,
13+ { id : "partners" , label : "Our Partners" } ,
14+ { id : "welcome" , label : "Welcome Aboard" } ,
15+ ] ;
16+
17+ export default function OnboardingStepper ( ) {
18+ const [ activeSection , setActiveSection ] = useState < string > ( SECTIONS [ 0 ] . id ) ;
19+
20+ useEffect ( ( ) => {
21+ // Intersection Observer to track which section is in view
22+ const observerOptions = {
23+ root : null ,
24+ rootMargin : "-50% 0px -50% 0px" , // Trigger when section crosses middle of viewport
25+ threshold : 0 ,
26+ } ;
27+
28+ const observerCallback = ( entries : IntersectionObserverEntry [ ] ) => {
29+ entries . forEach ( ( entry ) => {
30+ if ( entry . isIntersecting ) {
31+ setActiveSection ( entry . target . id ) ;
32+ }
33+ } ) ;
34+ } ;
35+
36+ const observer = new IntersectionObserver (
37+ observerCallback ,
38+ observerOptions
39+ ) ;
40+
41+ // Observe each section
42+ SECTIONS . forEach ( ( { id } ) => {
43+ const element = document . getElementById ( id ) ;
44+ if ( element ) observer . observe ( element ) ;
45+ } ) ;
46+
47+ return ( ) => observer . disconnect ( ) ;
48+ } , [ ] ) ;
49+
50+ // Scroll to section when circle is clicked
51+ const scrollToSection = ( sectionId : string ) => {
52+ const element = document . getElementById ( sectionId ) ;
53+ if ( element ) {
54+ element . scrollIntoView ( { behavior : "smooth" , block : "center" } ) ;
55+ }
56+ } ;
57+
58+ return (
59+ < nav
60+ className = "fixed left-6 top-1/2 z-50 hidden -translate-y-1/2 lg:block"
61+ aria-label = "Onboarding progress"
62+ >
63+ < div className = "flex flex-col gap-6" >
64+ { SECTIONS . map ( ( section , index ) => {
65+ const isActive = activeSection === section . id ;
66+ const isPassed = SECTIONS . findIndex ( ( s ) => s . id === activeSection ) > index ;
67+
68+ return (
69+ < button
70+ key = { section . id }
71+ onClick = { ( ) => scrollToSection ( section . id ) }
72+ className = "group relative flex items-center"
73+ aria-label = { `Go to ${ section . label } ` }
74+ aria-current = { isActive ? "step" : undefined }
75+ >
76+ { /* Circle indicator */ }
77+ < div
78+ className = { cn (
79+ "h-4 w-4 rounded-full border-2 transition-all duration-300" ,
80+ isActive && "scale-125 border-blue-600 bg-blue-600" ,
81+ isPassed && "border-blue-600 bg-blue-600" ,
82+ ! isActive && ! isPassed && "border-gray-400 bg-transparent"
83+ ) }
84+ />
85+
86+ { /* Tooltip label on hover */ }
87+ < span className = "absolute left-8 whitespace-nowrap rounded bg-gray-900 px-3 py-1.5 text-sm text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-100 dark:text-gray-900" >
88+ { section . label }
89+ </ span >
90+ </ button >
91+ ) ;
92+ } ) }
93+ </ div >
94+ </ nav >
95+ ) ;
96+ }
0 commit comments