|
1 | | -import React, { useState, useEffect, useRef, useCallback } from 'react'; |
2 | | -import { Link, useLocation } from 'react-router-dom'; |
3 | | -import { FaExternalLinkAlt, FaChevronRight } from 'react-icons/fa'; |
4 | | -import Dot from './Dot'; |
5 | | -import { useAnimation } from '../context/AnimationContext'; |
6 | | - |
7 | | -const ProjectCard = ({ project, size = 1 }) => { |
8 | | - const colSpanClass = |
9 | | - size === 2 ? 'md:col-span-2' : size === 3 ? 'md:col-span-3' : 'col-span-1'; |
10 | | - |
11 | | - const [dots, setDots] = useState([]); |
12 | | - const cardRef = useRef(null); |
13 | | - const dotIdRef = useRef(0); |
14 | | - const { |
15 | | - isAnimationEnabled, |
16 | | - showAnimationsHomepage, |
17 | | - showAnimationsInnerPages, |
18 | | - } = useAnimation(); |
19 | | - const location = useLocation(); |
20 | | - |
21 | | - const handleAnimationEnd = useCallback((id) => { |
22 | | - setDots((prevDots) => prevDots.filter((dot) => dot.id !== id)); |
23 | | - }, []); |
24 | | - |
25 | | - useEffect(() => { |
26 | | - let interval; |
27 | | - if ( |
28 | | - isAnimationEnabled && |
29 | | - ((location.pathname === '/' && showAnimationsHomepage) || |
30 | | - (location.pathname !== '/' && showAnimationsInnerPages)) |
31 | | - ) { |
32 | | - const spawnDot = () => { |
33 | | - if (cardRef.current && dots.length < 8) { |
34 | | - const cardRect = cardRef.current.getBoundingClientRect(); |
35 | | - const newDot = { |
36 | | - id: dotIdRef.current++, |
37 | | - size: Math.floor(Math.random() * 3) + 2, |
38 | | - // Cyan/Teal/Greenish Matrix tones |
39 | | - color: `hsl(${Math.floor(Math.random() * 60) + 160}, ${Math.floor(Math.random() * 50) + 50}%, ${Math.floor(Math.random() * 30) + 60}%)`, |
40 | | - initialX: Math.random() * cardRect.width, |
41 | | - initialY: -10, // Start slightly off-screen top |
42 | | - animationDuration: Math.random() * 4 + 3, |
43 | | - }; |
44 | | - setDots((prevDots) => [...prevDots, newDot]); |
45 | | - } |
46 | | - }; |
47 | | - |
48 | | - interval = setInterval(spawnDot, 600); |
49 | | - } |
50 | | - |
51 | | - return () => clearInterval(interval); |
52 | | - }, [ |
53 | | - dots.length, |
54 | | - isAnimationEnabled, |
55 | | - location.pathname, |
56 | | - showAnimationsHomepage, |
57 | | - showAnimationsInnerPages, |
58 | | - ]); |
| 1 | +import React from 'react'; |
| 2 | +import { Link } from 'react-router-dom'; |
| 3 | +import { motion } from 'framer-motion'; |
| 4 | +import { ArrowRight, CaretRight } from '@phosphor-icons/react'; |
59 | 5 |
|
| 6 | +const ProjectCard = ({ project, index, isActive, onHover = () => {} }) => { |
60 | 7 | return ( |
61 | | - <div |
62 | | - ref={cardRef} |
63 | | - className={`group relative flex flex-col overflow-hidden rounded-xl bg-gray-900/90 border border-gray-800 hover:border-cyan-500/50 transition-all duration-500 ease-out hover:shadow-[0_0_20px_-5px_rgba(6,182,212,0.4)] hover:-translate-y-1 ${colSpanClass} min-h-[300px]`} |
| 8 | + <motion.div |
| 9 | + initial={{ opacity: 0, x: -20 }} |
| 10 | + animate={{ opacity: 1, x: 0 }} |
| 11 | + transition={{ delay: index * 0.05 }} |
| 12 | + onMouseEnter={() => onHover(project)} |
| 13 | + className="relative" |
64 | 14 | > |
65 | | - {/* Background Grid Effect */} |
66 | | - <div className="absolute inset-0 z-0 opacity-20 pointer-events-none bg-[linear-gradient(to_right,#4f4f4f2e_1px,transparent_1px),linear-gradient(to_bottom,#4f4f4f2e_1px,transparent_1px)] bg-[size:14px_24px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_0%,#000_70%,transparent_100%)]" /> |
67 | | - |
68 | | - {/* Tech Accents - Top Right */} |
69 | | - <div className="absolute top-0 right-0 p-2 z-20"> |
70 | | - <div className="flex gap-1"> |
71 | | - <div className="w-1 h-1 rounded-full bg-gray-700 group-hover:bg-cyan-500/50 transition-colors duration-300 delay-75"></div> |
72 | | - <div className="w-1 h-1 rounded-full bg-gray-700 group-hover:bg-cyan-500/50 transition-colors duration-300 delay-100"></div> |
73 | | - <div className="w-1 h-1 rounded-full bg-gray-700 group-hover:bg-cyan-500/50 transition-colors duration-300 delay-150"></div> |
74 | | - </div> |
75 | | - </div> |
76 | | - |
77 | | - {/* Dots Layer */} |
78 | | - <div className="absolute inset-0 pointer-events-none z-0 overflow-hidden rounded-xl"> |
79 | | - {dots.map((dot) => ( |
80 | | - <Dot |
81 | | - key={dot.id} |
82 | | - id={dot.id} |
83 | | - size={dot.size} |
84 | | - color={dot.color} |
85 | | - initialX={dot.initialX} |
86 | | - initialY={dot.initialY} |
87 | | - animationDuration={dot.animationDuration} |
88 | | - onAnimationEnd={handleAnimationEnd} |
89 | | - /> |
90 | | - ))} |
91 | | - </div> |
92 | | - |
93 | | - {/* Watermark */} |
94 | | - <div className="absolute bottom-0 right-0 p-4 pointer-events-none z-0 opacity-5 group-hover:opacity-10 transition-opacity duration-500"> |
95 | | - <span className="text-6xl font-black font-mono text-cyan-500 tracking-tighter select-none"> |
96 | | - FCX |
97 | | - </span> |
98 | | - </div> |
99 | | - |
100 | | - {/* Content */} |
101 | 15 | <Link |
102 | 16 | to={`/projects/${project.slug}`} |
103 | | - className="flex flex-col flex-grow relative z-10 p-6" |
| 17 | + className="group relative flex items-center justify-between border-b border-white/10 py-8 pr-4 transition-all duration-300" |
104 | 18 | > |
105 | | - <div className="flex justify-between items-start mb-3"> |
106 | | - <h3 className="font-mono text-xl font-bold text-gray-100 group-hover:text-cyan-400 transition-colors tracking-tight"> |
| 19 | + {/* Active Indicator Line */} |
| 20 | + <div |
| 21 | + className={`absolute left-0 top-0 h-full w-1 bg-cyan-400 transition-all duration-300 ${ |
| 22 | + isActive ? 'opacity-100' : 'opacity-0' |
| 23 | + }`} |
| 24 | + /> |
| 25 | + |
| 26 | + <div className="flex items-baseline gap-6 pl-4 md:pl-8"> |
| 27 | + {/* Index / Year */} |
| 28 | + <span |
| 29 | + className={`font-mono text-sm transition-colors duration-300 ${ |
| 30 | + isActive ? 'text-cyan-400' : 'text-gray-600' |
| 31 | + }`} |
| 32 | + > |
| 33 | + {String(index + 1).padStart(2, '0')} |
| 34 | + </span> |
| 35 | + |
| 36 | + {/* Title */} |
| 37 | + <h3 |
| 38 | + className={`text-3xl font-light uppercase tracking-tight transition-all duration-300 md:text-5xl ${ |
| 39 | + isActive |
| 40 | + ? 'translate-x-4 text-white font-medium' |
| 41 | + : 'text-gray-500 group-hover:text-gray-300' |
| 42 | + }`} |
| 43 | + > |
107 | 44 | {project.title} |
108 | 45 | </h3> |
109 | 46 | </div> |
110 | 47 |
|
111 | | - <div className="h-px w-12 bg-gradient-to-r from-cyan-500 to-transparent mb-4 group-hover:w-full transition-all duration-500 ease-out" /> |
112 | | - |
113 | | - <p className="text-gray-400 text-sm leading-relaxed flex-grow font-sans"> |
114 | | - {project.description} |
115 | | - </p> |
116 | | - |
117 | | - <div className="mt-6 flex items-center justify-between"> |
118 | | - <div className="flex items-center gap-2"> |
119 | | - <span className="text-[10px] font-mono text-cyan-500/70 border border-cyan-900/30 bg-cyan-950/30 px-2 py-1 rounded uppercase tracking-wider"> |
120 | | - ID: {project.slug.split('-')[0].toUpperCase()} |
121 | | - </span> |
122 | | - </div> |
123 | | - <FaChevronRight |
124 | | - className="text-gray-600 group-hover:text-cyan-400 transform group-hover:translate-x-1 transition-all duration-300" |
125 | | - size={14} |
126 | | - /> |
| 48 | + {/* Arrow Interaction */} |
| 49 | + <div |
| 50 | + className={`transform transition-all duration-300 ${ |
| 51 | + isActive |
| 52 | + ? 'translate-x-0 opacity-100 text-cyan-400' |
| 53 | + : '-translate-x-4 opacity-0 text-gray-500' |
| 54 | + }`} |
| 55 | + > |
| 56 | + <ArrowRight size={32} weight="light" /> |
127 | 57 | </div> |
128 | 58 | </Link> |
129 | | - |
130 | | - {/* External Link footer if exists */} |
131 | | - {project.link && ( |
132 | | - <div className="relative z-10 px-6 pb-4"> |
133 | | - <a |
134 | | - href={project.link} |
135 | | - target="_blank" |
136 | | - rel="noopener noreferrer" |
137 | | - className="inline-flex items-center gap-2 text-[10px] font-bold text-gray-500 hover:text-cyan-400 uppercase tracking-widest transition-colors border-t border-gray-800 pt-3 w-full" |
138 | | - > |
139 | | - <span className="w-1.5 h-1.5 bg-current rounded-full animate-pulse" /> |
140 | | - Live System |
141 | | - <FaExternalLinkAlt size={9} className="ml-auto" /> |
142 | | - </a> |
143 | | - </div> |
144 | | - )} |
145 | | - </div> |
| 59 | + </motion.div> |
146 | 60 | ); |
147 | 61 | }; |
148 | 62 |
|
|
0 commit comments