Skip to content

Commit 545765b

Browse files
committed
resume animation playing
1 parent bf99400 commit 545765b

File tree

4 files changed

+190
-19
lines changed

4 files changed

+190
-19
lines changed

package-lock.json

Lines changed: 43 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dependencies": {
1414
"@astrojs/react": "^4.3.0",
1515
"astro": "^5.2.3",
16+
"framer-motion": "^12.23.12",
1617
"react": "^19.1.0",
1718
"react-dom": "^19.1.0"
1819
},

src/components/react/ResumeTimeline.css

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@
2727
.resume-timeline__container {
2828
position: relative;
2929
padding-left: 0;
30+
overflow: visible;
31+
}
32+
33+
/* Allow for slide-in animations from outside the viewport */
34+
.timeline-item {
35+
overflow: visible;
3036
}
3137

3238
/* Timeline Item Layout */
@@ -314,6 +320,56 @@
314320
.content-card:hover {
315321
transform: none;
316322
}
323+
324+
/* Disable all Framer Motion animations for users who prefer reduced motion */
325+
* {
326+
animation-duration: 0.01ms !important;
327+
animation-iteration-count: 1 !important;
328+
transition-duration: 0.01ms !important;
329+
}
330+
}
331+
332+
/* Enhanced Animation Support */
333+
.timeline-item {
334+
/* Improve performance during animations */
335+
will-change: transform, opacity;
336+
}
337+
338+
.company-avatar {
339+
/* Add subtle glow effect on hover for enhanced visual feedback */
340+
transition: box-shadow 0.3s ease;
341+
}
342+
343+
.company-avatar:hover {
344+
box-shadow: 0 2px 12px rgba(var(--accent-rgb), 0.4),
345+
0 4px 20px rgba(var(--accent-rgb), 0.2);
346+
}
347+
348+
/* Smooth transforms for better animation performance */
349+
.content-card,
350+
.company-avatar,
351+
.timeline-line {
352+
transform-style: preserve-3d;
353+
backface-visibility: hidden;
354+
}
355+
356+
/* Enhanced hover states that work with Framer Motion */
357+
.content-card {
358+
transition: box-shadow 0.3s ease;
359+
position: relative;
360+
}
361+
362+
.content-card:hover {
363+
/* Enhanced shadow for better depth perception */
364+
box-shadow: 0 6px 20px rgba(var(--gray), 0.25),
365+
0 12px 40px rgba(var(--gray), 0.15),
366+
0 0 0 1px rgba(var(--accent-rgb), 0.1);
367+
}
368+
369+
/* Enhanced hover effect styling - backdrop removed for better readability */
370+
.timeline-item:hover .content-card {
371+
position: relative;
372+
z-index: 1000;
317373
}
318374

319375
/* Focus States for Accessibility */

src/components/react/ResumeTimeline.tsx

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import { motion, useInView } from 'framer-motion';
23
import { resumeEvents, type ResumeEvent } from '../../data/resumeEvents';
34
import './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

Comments
 (0)