11import React from 'react' ;
2+ import { motion , useInView } from 'framer-motion' ;
23import { resumeEvents , type ResumeEvent } from '../../data/resumeEvents' ;
34import './ResumeTimeline.css' ;
45
@@ -11,9 +12,13 @@ interface ResumeTimelineItemProps {
1112 event : ResumeEvent ;
1213 isFirst : boolean ;
1314 isLast : boolean ;
15+ index : number ;
1416}
1517
16- const ResumeTimelineItem : React . FC < ResumeTimelineItemProps > = ( { event, isFirst, isLast } ) => {
18+ const ResumeTimelineItem : React . FC < ResumeTimelineItemProps > = ( { event, isFirst, isLast, index } ) => {
19+ const ref = React . useRef ( null ) ;
20+ const isInView = useInView ( ref , { once : true , margin : "-20%" } ) ;
21+
1722 const formatDateRange = ( startDate : string , endDate : string | null ) => {
1823 const formatDate = ( dateString : string ) => {
1924 const [ year , month ] = dateString . split ( '-' ) ;
@@ -34,51 +39,117 @@ const ResumeTimelineItem: React.FC<ResumeTimelineItemProps> = ({ event, isFirst,
3439 . substring ( 0 , 2 ) ;
3540 } ;
3641
42+ // Determine slide direction based on index (odd/even)
43+ const isOdd = index % 2 === 1 ;
44+ const slideX = isOdd ? - 400 : 400 ; // Odd slides from left, even slides from right (using large fixed values for SSR compatibility)
45+
3746 return (
38- < div className = { `timeline-item ${ isFirst ? 'timeline-item--first' : '' } ${ isLast ? 'timeline-item--last' : '' } ` } >
47+ < motion . div
48+ ref = { ref }
49+ className = { `timeline-item ${ isFirst ? 'timeline-item--first' : '' } ${ isLast ? 'timeline-item--last' : '' } ` }
50+ initial = { { opacity : 0 , x : slideX , scale : 0.95 } }
51+ animate = { isInView ? { opacity : 1 , x : 0 , scale : 1 } : { opacity : 0 , x : slideX , scale : 0.95 } }
52+ transition = { { duration : 0.6 , ease : "easeOut" } }
53+ >
3954 < div className = "timeline-connector" >
40- < div className = "company-avatar" >
55+ < motion . div
56+ className = "company-avatar"
57+ initial = { { scale : 0 , rotate : - 180 } }
58+ animate = { isInView ? { scale : 1 , rotate : 0 } : { scale : 0 , rotate : - 180 } }
59+ transition = { { type : "spring" , stiffness : 200 , damping : 10 , delay : 0.2 } }
60+ >
4161 < span className = "company-avatar__initials" >
4262 { getCompanyInitials ( event . company ) }
4363 </ span >
44- </ div >
45- { ! isLast && < div className = "timeline-line" > </ div > }
64+ </ motion . div >
65+ { ! isLast && (
66+ < motion . div
67+ className = "timeline-line"
68+ initial = { { scaleY : 0 , opacity : 0 } }
69+ animate = { isInView ? { scaleY : 1 , opacity : 1 } : { scaleY : 0 , opacity : 0 } }
70+ transition = { { duration : 0.8 , ease : "easeOut" , delay : 0.5 } }
71+ style = { { originY : 0 } }
72+ > </ motion . div >
73+ ) }
4674 </ div >
4775
48- < div className = "content-card" >
76+ < motion . div
77+ className = "content-card"
78+ initial = { { opacity : 0 , x : 60 , scale : 0.95 } }
79+ animate = { isInView ? { opacity : 1 , x : 0 , scale : 1 } : { opacity : 0 , x : 60 , scale : 0.95 } }
80+ transition = { { duration : 0.5 , ease : "easeOut" , delay : 0.3 } }
81+ >
4982 < div className = "content-card__header" >
5083 < div className = "content-card__title-section" >
51- < h3 className = "job-title" > { event . title } </ h3 >
52- < div className = "company-info" >
84+ < motion . h3
85+ className = "job-title"
86+ initial = { { opacity : 0 } }
87+ animate = { isInView ? { opacity : 1 } : { opacity : 0 } }
88+ transition = { { delay : 0.5 , duration : 0.3 } }
89+ >
90+ { event . title }
91+ </ motion . h3 >
92+ < motion . div
93+ className = "company-info"
94+ initial = { { opacity : 0 } }
95+ animate = { isInView ? { opacity : 1 } : { opacity : 0 } }
96+ transition = { { delay : 0.6 , duration : 0.3 } }
97+ >
5398 < span className = "company-name" > { event . company } </ span >
5499 < span className = "company-location" > { event . location } </ span >
55- </ div >
100+ </ motion . div >
56101 </ div >
57- < div className = "date-range" >
102+ < motion . div
103+ className = "date-range"
104+ initial = { { opacity : 0 } }
105+ animate = { isInView ? { opacity : 1 } : { opacity : 0 } }
106+ transition = { { delay : 0.7 , duration : 0.3 } }
107+ >
58108 { formatDateRange ( event . startDate , event . endDate ) }
59- </ div >
109+ </ motion . div >
60110 </ div >
61111
62- < div className = "description" >
112+ < motion . div
113+ className = "description"
114+ initial = { { opacity : 0 } }
115+ animate = { isInView ? { opacity : 1 } : { opacity : 0 } }
116+ transition = { { delay : 0.8 , duration : 0.4 } }
117+ >
63118 { event . description . split ( '\n\n' ) . map ( ( paragraph , index ) => {
64119 // Check if this paragraph starts with a position title
65120 const positionMatch = paragraph . match ( / ^ ( [ ^ : ] + ) : / ) ;
66121 if ( positionMatch ) {
67122 const [ fullMatch , positionTitle ] = positionMatch ;
68123 const description = paragraph . substring ( fullMatch . length ) . trim ( ) ;
69124 return (
70- < div key = { index } className = "position-section" >
125+ < motion . div
126+ key = { index }
127+ className = "position-section"
128+ initial = { { opacity : 0 , y : 10 } }
129+ animate = { isInView ? { opacity : 1 , y : 0 } : { opacity : 0 , y : 10 } }
130+ transition = { { delay : 0.9 + index * 0.1 , duration : 0.3 } }
131+ >
71132 < h4 className = "position-title" > { positionTitle } </ h4 >
72133 < p className = "position-description" > { description } </ p >
73- </ div >
134+ </ motion . div >
74135 ) ;
75136 } else {
76- return < p key = { index } className = "position-description" > { paragraph } </ p > ;
137+ return (
138+ < motion . p
139+ key = { index }
140+ className = "position-description"
141+ initial = { { opacity : 0 , y : 10 } }
142+ animate = { isInView ? { opacity : 1 , y : 0 } : { opacity : 0 , y : 10 } }
143+ transition = { { delay : 0.9 + index * 0.1 , duration : 0.3 } }
144+ >
145+ { paragraph }
146+ </ motion . p >
147+ ) ;
77148 }
78149 } ) }
79- </ div >
80- </ div >
81- </ div >
150+ </ motion . div >
151+ </ motion . div >
152+ </ motion . div >
82153 ) ;
83154} ;
84155
@@ -99,6 +170,7 @@ const ResumeTimeline: React.FC<ResumeTimelineProps> = ({
99170 < ResumeTimelineItem
100171 key = { event . id }
101172 event = { event }
173+ index = { index }
102174 isFirst = { index === 0 }
103175 isLast = { index === events . length - 1 }
104176 />
0 commit comments