Skip to content

Commit 5977157

Browse files
committed
feat: projects
1 parent 81518a6 commit 5977157

File tree

3 files changed

+187
-204
lines changed

3 files changed

+187
-204
lines changed

src/components/ProjectCard.js

Lines changed: 47 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,62 @@
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';
595

6+
const ProjectCard = ({ project, index, isActive, onHover = () => {} }) => {
607
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"
6414
>
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 */}
10115
<Link
10216
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"
10418
>
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+
>
10744
{project.title}
10845
</h3>
10946
</div>
11047

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" />
12757
</div>
12858
</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>
14660
);
14761
};
14862

src/pages/HomePage.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ const HomePage = () => {
135135
const [posts, setPosts] = useState([]);
136136
const [loadingPosts, setLoadingPosts] = useState(true);
137137
const { projects: pinnedProjects, loading: loadingProjects } = useProjects(true);
138+
const [activeProject, setActiveProject] = useState(null);
138139

139140
// Use persistent state for homepage section order
140141
const [homepageSectionOrder] = usePersistentState(KEY_HOMEPAGE_SECTION_ORDER, ['projects', 'blogposts']);
@@ -190,17 +191,15 @@ const HomePage = () => {
190191
return (
191192
<section className="mb-24 mt-8">
192193
<SectionHeader icon={Cpu} title="Pinned Projects" link="/projects" linkText="View all projects" />
193-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
194+
<div className="flex flex-col border-t border-white/10">
194195
{pinnedProjects.map((project, index) => (
195-
<motion.div
196+
<ProjectCard
196197
key={project.slug}
197-
initial={{ opacity: 0, y: 20 }}
198-
whileInView={{ opacity: 1, y: 0 }}
199-
transition={{ delay: index * 0.1 }}
200-
viewport={{ once: true }}
201-
>
202-
<ProjectCard project={{ ...project, description: project.shortDescription }} />
203-
</motion.div>
198+
project={project}
199+
index={index}
200+
isActive={activeProject?.slug === project.slug}
201+
onHover={setActiveProject}
202+
/>
204203
))}
205204
</div>
206205
</section>

0 commit comments

Comments
 (0)