feat: Add Skills page, BioSection, ProjectCard, and new image assets, while refactoring the deployment workflow to build Docker images directly on my remote server.
This commit is contained in:
100
src/components/BioSection.tsx
Normal file
100
src/components/BioSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
62
src/components/ProjectCard.tsx
Normal file
62
src/components/ProjectCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user