feat: Implement Skills page, ProjectCard and BioSection components, and update navigation and About page content.

This commit is contained in:
2026-01-05 22:23:30 -06:00
parent bd671155a3
commit 2e6fb7fa49
14 changed files with 488 additions and 135 deletions

View File

@@ -0,0 +1,100 @@
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
interface BioSectionProps {
imageSrc: string;
imageAlt: string;
text: string;
reversed?: boolean;
}
export default function BioSection({ imageSrc, imageAlt, text, reversed = false }: BioSectionProps) {
const images = imageSrc.split(',').map(src => src.trim()).filter(src => src.length > 0);
const [currentIndex, setCurrentIndex] = useState(0);
const [prevIndex, setPrevIndex] = useState(0);
const [direction, setDirection] = useState(1); // 1 for right, -1 for left (though we always slide right here)
useEffect(() => {
if (images.length <= 1) return;
const interval = setInterval(() => {
setPrevIndex(currentIndex);
setCurrentIndex((prev) => (prev + 1) % images.length);
}, 6000);
return () => clearInterval(interval);
}, [images.length, currentIndex]);
const slideVariants = {
enter: {
x: "-100%",
opacity: 1
},
center: {
x: 0,
opacity: 1
},
exit: {
x: "100%",
opacity: 1
}
};
return (
<div
className="about-content"
style={{
flexDirection: reversed ? "row-reverse" : "row",
}}
>
<motion.div
className="about-image-container"
initial={{ x: reversed ? 30 : -30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.6 }}
style={{
position: "relative",
width: "250px",
height: "250px",
overflow: "hidden",
borderRadius: "20px",
border: "3px solid rgba(255, 255, 255, 0.8)",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
}}
>
<AnimatePresence initial={false}>
<motion.img
key={currentIndex}
src={images[currentIndex]}
alt={imageAlt}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.8, ease: "easeInOut" }}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "cover",
}}
whileHover={{ scale: 1.05 }}
/>
</AnimatePresence>
</motion.div>
<motion.div
className="about-text-container"
initial={{ x: reversed ? -30 : 30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
<p className="about-text">
{text}
</p>
</motion.div>
</div>
);
}

View File

@@ -35,31 +35,6 @@ const ParticlesBackground: React.FC = () => {
this.x += this.speedX;
this.y += this.speedY;
// update speedX
let speedXRng = Math.random();
if (speedXRng > 0.75) {
this.speedX += Math.random() * 0.1;
} else if (speedXRng < 0.25) {
this.speedX -= Math.random() * 0.1;
}
if (this.speedX > 1) {
this.speedX = 1;
} else if (this.speedX < -1) {
this.speedX = -1;
}
// update speedY
let speedYRng = Math.random();
if (speedYRng > 0.75) {
this.speedY += Math.random() * 0.1;
} else if (speedYRng < 0.25) {
this.speedY -= Math.random() * 0.1;
}
if (this.speedY > 1) {
this.speedY = 1;
} else if (this.speedY < -1) {
this.speedY = -1;
}
//size
let sizeRng = Math.random();

View File

@@ -0,0 +1,62 @@
import { motion, Variants } from "framer-motion";
export interface Project {
id: number;
title: string;
description: string;
techStack: string[];
image: string;
links: {
demo?: string;
repo?: string;
};
}
interface ProjectCardProps {
project: Project;
variants: Variants;
}
export default function ProjectCard({ project, variants }: ProjectCardProps) {
return (
<motion.div
className="project-card"
variants={variants}
whileHover={{ y: -10, transition: { duration: 0.2 } }}
>
<div className="project-image-container">
<img
src={project.image}
alt={project.title}
className="project-image"
/>
</div>
<div className="project-header">
<h2 className="project-name">{project.title}</h2>
</div>
<div className="project-tech-stack">
{project.techStack.map(tech => (
<span key={tech} className="tech-chip">{tech}</span>
))}
</div>
<p className="project-description">
{project.description}
</p>
<div className="project-links">
{project.links.demo && (
<a href={project.links.demo} className="project-link" target="_blank" rel="noopener noreferrer">
Live Demo <span></span>
</a>
)}
{project.links.repo && (
<a href={project.links.repo} className="project-link" target="_blank" rel="noopener noreferrer">
GitHub <span></span>
</a>
)}
</div>
</motion.div>
);
}

View File

@@ -6,32 +6,38 @@ export default function FloatingHeader() {
return (
<header className="floating-header">
<nav className="header-nav">
<Link
to="/"
<Link
to="/"
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
>
Home
</Link>
<Link
to="/work-experience"
<Link
to="/work-experience"
className={`nav-link ${location.pathname === "/work-experience" ? "active" : ""}`}
>
Work Experience
</Link>
<Link
to="/about"
<Link
to="/skills"
className={`nav-link ${location.pathname === "/skills" ? "active" : ""}`}
>
Skills
</Link>
<Link
to="/about"
className={`nav-link ${location.pathname === "/about" ? "active" : ""}`}
>
About Me
</Link>
<Link
to="/projects"
<Link
to="/projects"
className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
>
Projects
Projects and Sidequests
</Link>
<Link
to="/contact"
<Link
to="/contact"
className={`nav-link ${location.pathname === "/contact" ? "active" : ""}`}
>
Contact