@@ -75,13 +75,37 @@ const projectsData = [
75
75
// ProjectCard component
76
76
const ProjectCard = ( { project, index, openModal } ) => {
77
77
const [ isHovered , setIsHovered ] = useState ( false ) ;
78
+ const cardRef = useRef ( null ) ;
79
+
80
+ useEffect ( ( ) => {
81
+ const observer = new IntersectionObserver (
82
+ ( entries ) => {
83
+ entries . forEach ( ( entry ) => {
84
+ if ( entry . isIntersecting ) {
85
+ entry . target . classList . add (
86
+ index % 2 === 0 ? 'animate-slide-in-card-left' : 'animate-slide-in-card-right'
87
+ ) ;
88
+ observer . unobserve ( entry . target ) ;
89
+ }
90
+ } ) ;
91
+ } ,
92
+ { threshold : 0.2 }
93
+ ) ;
94
+
95
+ if ( cardRef . current ) {
96
+ observer . observe ( cardRef . current ) ;
97
+ }
98
+
99
+ return ( ) => observer . disconnect ( ) ;
100
+ } , [ index ] ) ;
78
101
79
102
return (
80
103
< div
104
+ ref = { cardRef }
81
105
className = { `group relative overflow-hidden bg-gradient-to-br from-gray-900/90 to-gray-800/90
82
106
backdrop-blur-lg rounded-xl shadow-lg transition-all duration-500 hover:shadow-2xl
83
107
hover:shadow-blue-500/20 border border-gray-700/50 hover:border-blue-500/50
84
- transform hover:-translate-y-1` }
108
+ transform hover:-translate-y-1 opacity-0 flex flex-col h-full ` }
85
109
onMouseEnter = { ( ) => setIsHovered ( true ) }
86
110
onMouseLeave = { ( ) => setIsHovered ( false ) }
87
111
>
@@ -102,76 +126,82 @@ const ProjectCard = ({ project, index, openModal }) => {
102
126
{ /* Project status badge */ }
103
127
{ project . status && (
104
128
< span className = "absolute top-4 right-4 px-3 py-1.5 text-xs font-semibold rounded-full
105
- bg-green-500/20 text-green-400 backdrop-blur-sm border border-green-500/30" >
129
+ bg-green-500/20 text-green-400 backdrop-blur-sm border border-green-500/30 animate-scale-in " >
106
130
{ project . status }
107
131
</ span >
108
132
) }
109
133
</ div >
110
134
111
- { /* Content section */ }
112
- < div className = "relative p-6 space-y-6" >
113
- { /* Title and buttons */ }
114
- < div className = "flex justify-between items-start gap-4" >
115
- < h3 className = "text-2xl font-bold text-white group-hover:text-blue-400
116
- transition-colors duration-300" >
117
- { project . title }
118
- </ h3 >
119
- < div className = "flex gap-2" >
120
- < a
121
- href = { project . github }
122
- target = "_blank"
123
- rel = "noopener noreferrer"
124
- className = "p-2 rounded-lg bg-gray-700/50 hover:bg-gray-600 transition-all
125
- duration-300 hover:scale-110 hover:shadow-lg hover:shadow-blue-500/20"
126
- >
127
- < Github size = { 20 } className = "text-gray-300 hover:text-white" />
128
- </ a >
135
+ { /* Content wrapper - Added to create consistent spacing */ }
136
+ < div className = "flex flex-col flex-1 p-6" >
137
+ { /* Main content section */ }
138
+ < div className = "flex flex-col flex-1 space-y-6" >
139
+ { /* Title and buttons */ }
140
+ < div className = "flex justify-between items-start gap-4" >
141
+ < h3 className = "text-2xl font-bold text-white group-hover:text-blue-400
142
+ transition-colors duration-300" >
143
+ { project . title }
144
+ </ h3 >
145
+ < div className = "flex gap-2" >
146
+ < a
147
+ href = { project . github }
148
+ target = "_blank"
149
+ rel = "noopener noreferrer"
150
+ className = "p-2 rounded-lg bg-gray-700/50 hover:bg-gray-600 transition-all
151
+ duration-300 hover:scale-110 hover:shadow-lg hover:shadow-blue-500/20"
152
+ >
153
+ < Github size = { 20 } className = "text-gray-300 hover:text-white" />
154
+ </ a >
155
+ </ div >
129
156
</ div >
130
- </ div >
131
157
132
- { /* Description */ }
133
- < div className = "prose prose-invert prose-sm max-w-none" >
134
- < ReactMarkdown remarkPlugins = { [ remarkGfm ] } className = "line-clamp-3" >
135
- { project . description . split ( '\n' ) [ 0 ] }
136
- </ ReactMarkdown >
137
- </ div >
158
+ { /* Description */ }
159
+ < div className = "prose prose-invert prose-sm max-w-none" >
160
+ < ReactMarkdown remarkPlugins = { [ remarkGfm ] } className = "line-clamp-3" >
161
+ { project . description . split ( '\n' ) [ 0 ] }
162
+ </ ReactMarkdown >
163
+ </ div >
138
164
139
- { /* Tech stack */ }
140
- < div className = "space-y-3" >
141
- < h4 className = "text-sm font-medium text-gray-400 uppercase tracking-wider" >
142
- Technologies
143
- </ h4 >
144
- < div className = "flex flex-wrap gap-2" >
145
- { project . technologies . map ( ( tech , i ) => (
146
- < span
147
- key = { tech }
148
- className = "px-3 py-1.5 text-xs font-medium rounded-full bg-blue-500/10
149
- text-blue-400 border border-blue-500/30 hover:bg-blue-500/20
150
- transition-all duration-300 transform hover:scale-105"
151
- style = { {
152
- transitionDelay : `${ i * 50 } ms` ,
153
- animation : isHovered ? `fadeIn 500ms ${ i * 50 } ms forwards` : 'none' ,
154
- } }
155
- >
156
- { tech }
157
- </ span >
158
- ) ) }
165
+ { /* Tech stack */ }
166
+ < div className = "space-y-3" >
167
+ < h4 className = "text-sm font-medium text-gray-400 uppercase tracking-wider" >
168
+ Technologies
169
+ </ h4 >
170
+ < div className = "flex flex-wrap gap-2" >
171
+ { project . technologies . map ( ( tech , i ) => (
172
+ < span
173
+ key = { tech }
174
+ className = "px-3 py-1.5 text-xs font-medium rounded-full bg-blue-500/10
175
+ text-blue-400 border border-blue-500/30 hover:bg-blue-500/20
176
+ transition-all duration-300 transform hover:scale-105 animate-tech-tag-pop"
177
+ style = { {
178
+ transitionDelay : `${ i * 50 } ms` ,
179
+ animationDelay : `${ i * 50 } ms` ,
180
+ } }
181
+ >
182
+ { tech }
183
+ </ span >
184
+ ) ) }
185
+ </ div >
159
186
</ div >
160
187
</ div >
161
188
162
- { /* View details button */ }
163
- < button
164
- onClick = { ( ) => openModal ( project ) }
165
- className = "w-full mt-4 px-4 py-3 flex items-center justify-center gap-2
166
- bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-500
167
- hover:to-blue-400 text-white rounded-lg transition-all duration-300
168
- transform hover:scale-[1.02] hover:shadow-lg hover:shadow-blue-500/25
169
- focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900"
170
- >
171
- < Code size = { 18 } />
172
- View Project Details
173
- < ExternalLink size = { 18 } className = "ml-1" />
174
- </ button >
189
+ { /* Button section - Now in a separate div outside the flex-1 content */ }
190
+ < div className = "pt-6" >
191
+ < button
192
+ onClick = { ( ) => openModal ( project ) }
193
+ className = "w-full px-4 py-3 flex items-center justify-center gap-2
194
+ bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-500
195
+ hover:to-blue-400 text-white rounded-lg transition-all duration-300
196
+ transform hover:scale-[1.02] hover:shadow-lg hover:shadow-blue-500/25
197
+ focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-900 animate-fade-up"
198
+ style = { { animationDelay : '200ms' } }
199
+ >
200
+ < Code size = { 18 } />
201
+ View Project Details
202
+ < ExternalLink size = { 18 } className = "ml-1" />
203
+ </ button >
204
+ </ div >
175
205
</ div >
176
206
</ div >
177
207
) ;
@@ -256,53 +286,6 @@ const Projects = () => {
256
286
return ( ) => observer . disconnect ( ) ;
257
287
} , [ ] ) ;
258
288
259
- // Project cards animation observer
260
- useEffect ( ( ) => {
261
- const projectCards = document . querySelectorAll ( '.project-card' ) ;
262
- const observer = new IntersectionObserver (
263
- ( entries ) => {
264
- entries . forEach ( ( entry ) => {
265
- if ( entry . isIntersecting ) {
266
- const card = entry . target ;
267
- const projectTitle = card . querySelector ( 'h3' ) . textContent ;
268
- const project = projectsData . find ( ( p ) => p . title === projectTitle ) ;
269
- const index = Array . from ( projectCards ) . indexOf ( card ) ;
270
-
271
- if ( ! animatedProjects . includes ( project . title ) ) {
272
- if ( index % 2 === 0 ) {
273
- card . classList . add ( 'animate-slide-in-card-left' ) ;
274
- } else {
275
- card . classList . add ( 'animate-slide-in-card-right' ) ;
276
- }
277
-
278
- const tags = card . querySelectorAll ( '.tech-tag' ) ;
279
- tags . forEach ( ( tag , i ) => {
280
- tag . style . transitionDelay = `${ ( index * 200 ) + ( i * 50 ) } ms` ;
281
- tag . classList . add ( 'opacity-100' , 'scale-100' ) ;
282
- } ) ;
283
-
284
- setAnimatedProjects ( ( prev ) => [ ...prev , project . title ] ) ;
285
- }
286
- }
287
- } ) ;
288
- } ,
289
- { threshold : 0.2 }
290
- ) ;
291
-
292
- projectCards . forEach ( ( card ) => observer . observe ( card ) ) ;
293
- return ( ) => observer . disconnect ( ) ;
294
- } , [ animatedProjects ] ) ;
295
-
296
- // Helper function to get the first paragraph and format it
297
- const getShortDescription = ( description ) => {
298
- const firstParagraph = description . split ( '\n' ) [ 0 ] ;
299
- return (
300
- < ReactMarkdown remarkPlugins = { [ remarkGfm ] } className = "text-gray-300 mb-6 line-clamp-3" >
301
- { firstParagraph }
302
- </ ReactMarkdown >
303
- ) ;
304
- } ;
305
-
306
289
const openModal = ( project ) => {
307
290
setSelectedProject ( project ) ;
308
291
setModalOpen ( true ) ;
0 commit comments