@@ -13,39 +13,28 @@ const SKILL_LEVELS = [
1313
1414const BAR_TOTAL = 20 ;
1515
16- const SkillBar = ( { name, level, delay } : { name : string ; level : number ; delay : number } ) => {
16+ const SkillBar = ( { name, level, delay, started } : { name : string ; level : number ; delay : number ; started : boolean } ) => {
1717 const [ filled , setFilled ] = useState ( 0 ) ;
18- const ref = useRef < HTMLDivElement > ( null ) ;
1918 const target = Math . round ( ( level / 100 ) * BAR_TOTAL ) ;
2019
2120 useEffect ( ( ) => {
22- const el = ref . current ;
23- if ( ! el ) return ;
24-
25- const observer = new IntersectionObserver (
26- ( [ entry ] ) => {
27- if ( ! entry . isIntersecting ) return ;
28- observer . disconnect ( ) ;
29- let current = 0 ;
30- const step = ( ) => {
31- if ( current >= target ) return ;
32- current ++ ;
33- setFilled ( current ) ;
34- setTimeout ( step , 45 ) ;
35- } ;
36- setTimeout ( step , delay ) ;
37- } ,
38- { threshold : 0.5 }
39- ) ;
40- observer . observe ( el ) ;
41- return ( ) => observer . disconnect ( ) ;
42- } , [ target , delay ] ) ;
21+ if ( ! started ) return ;
22+ let current = 0 ;
23+ let timer : ReturnType < typeof setTimeout > ;
24+ const step = ( ) => {
25+ if ( current >= target ) return ;
26+ current ++ ;
27+ setFilled ( current ) ;
28+ timer = setTimeout ( step , 45 ) ;
29+ } ;
30+ timer = setTimeout ( step , delay ) ;
31+ return ( ) => clearTimeout ( timer ) ;
32+ } , [ started , target , delay ] ) ;
4333
4434 const empty = BAR_TOTAL - filled ;
4535
4636 return (
4737 < div
48- ref = { ref }
4938 style = { {
5039 display : "flex" ,
5140 alignItems : "center" ,
@@ -158,6 +147,25 @@ const AutoTypeCLI = () => {
158147const ProfileSection = ( ) => {
159148 const skills = personalInfo . skills ;
160149
150+ // Single IntersectionObserver for all skill bars
151+ const [ skillsStarted , setSkillsStarted ] = useState ( false ) ;
152+ const skillsContainerRef = useRef < HTMLDivElement > ( null ) ;
153+
154+ useEffect ( ( ) => {
155+ const el = skillsContainerRef . current ;
156+ if ( ! el ) return ;
157+ const observer = new IntersectionObserver (
158+ ( [ entry ] ) => {
159+ if ( ! entry . isIntersecting ) return ;
160+ observer . disconnect ( ) ;
161+ setSkillsStarted ( true ) ;
162+ } ,
163+ { threshold : 0.3 }
164+ ) ;
165+ observer . observe ( el ) ;
166+ return ( ) => observer . disconnect ( ) ;
167+ } , [ ] ) ;
168+
161169 // Chained typewriter: name first, then role after name finishes
162170 const [ nameTyped , setNameTyped ] = useState ( false ) ;
163171 const { displayed : displayedName } = useTypewriter ( personalInfo . name , {
@@ -391,12 +399,12 @@ const ProfileSection = () => {
391399 </ div >
392400
393401 { /* Skills progress bars */ }
394- < div style = { { marginBottom : "12px" } } >
402+ < div ref = { skillsContainerRef } style = { { marginBottom : "12px" } } >
395403 < div style = { { fontSize : "11px" , color : "var(--term-dim)" , marginBottom : "8px" } } >
396404 # proficiency
397405 </ div >
398406 { SKILL_LEVELS . map ( ( s , i ) => (
399- < SkillBar key = { s . name } name = { s . name } level = { s . level } delay = { i * 120 } />
407+ < SkillBar key = { s . name } name = { s . name } level = { s . level } delay = { i * 120 } started = { skillsStarted } />
400408 ) ) }
401409 </ div >
402410
0 commit comments