feat(portfolio): replace skills section with about section and skills marquee

- Remove old skills data and components
- Add new about section with personal introduction and stats
- Implement animated skills marquee component
- Update navigation to reflect new section structure
This commit is contained in:
joyzhao
2025-06-16 12:50:08 +08:00
parent 0c22c6abf6
commit a18a0cdff1
10 changed files with 432 additions and 346 deletions

View File

@@ -0,0 +1,198 @@
import { motion, useAnimation } from "framer-motion";
import { useState, useEffect } from "react";
import { useTranslations } from "@/i18n/utils";
/**
* Skill item interface for the marquee component
*/
interface SkillItem {
name: string;
icon: string; // skillicons icon name
}
/**
* All skills data with corresponding skillicons names
* Using skillicons.dev for consistent icon display
*/
const allSkills: SkillItem[] = [
// Programming Languages
{ name: "TypeScript", icon: "typescript" },
{ name: "JavaScript", icon: "javascript" },
{ name: "React", icon: "react" },
{ name: "Vue", icon: "vue" },
{ name: "微信小程序", icon: "wechat" },
{ name: "UniApp", icon: "vue" }, // Using vue icon as fallback
// Frontend Development
{ name: "Next.js", icon: "nextjs" },
{ name: "Nuxt.js", icon: "nuxtjs" },
{ name: "React Native", icon: "react" },
{ name: "Tailwind CSS", icon: "tailwindcss" },
{ name: "Shadcn UI", icon: "react" }, // Using react icon as fallback
{ name: "PrimeVue", icon: "vue" }, // Using vue icon as fallback
{ name: "Naive-UI", icon: "vue" }, // Using vue icon as fallback
// Backend Development
{ name: "Node.js", icon: "nodejs" },
{ name: "Express.js", icon: "express" },
{ name: "Nest.js", icon: "nestjs" },
{ name: "Fastify.js", icon: "fastify" },
{ name: "Hono.js", icon: "nodejs" }, // Using nodejs icon as fallback
// Database & Storage
{ name: "PostgreSQL", icon: "postgresql" },
{ name: "MongoDB", icon: "mongodb" },
{ name: "Drizzle ORM", icon: "typescript" }, // Using typescript icon as fallback
{ name: "Mongoose", icon: "mongodb" }, // Using mongodb icon as fallback
{ name: "Prisma", icon: "prisma" },
// Cloud & DevOps
{ name: "AWS", icon: "aws" },
{ name: "Cloudflare", icon: "cloudflare" },
{ name: "Vercel", icon: "vercel" },
// Tools & Services
{ name: "Zod", icon: "typescript" }, // Using typescript icon as fallback
{ name: "BetterAuth", icon: "nodejs" }, // Using nodejs icon as fallback
{ name: "Clerk", icon: "react" }, // Using react icon as fallback
{ name: "GitLab", icon: "gitlab" },
{ name: "CI/CD", icon: "github" }, // Using github icon as fallback
{ name: "Git", icon: "git" },
{ name: "Docker", icon: "docker" },
];
/**
* Individual skill item component with icon and name
*/
function SkillItem({ skill }: { skill: SkillItem }) {
const iconUrl = `https://skillicons.dev/icons?i=${skill.icon}`;
return (
<div className="flex items-center gap-3 px-4 py-2 bg-muted/80 backdrop-blur-sm rounded-lg border border-purple-500/10 shadow-sm whitespace-nowrap flex-shrink-0">
<img
src={iconUrl}
alt={skill.name}
className="w-6 h-6 object-contain flex-shrink-0"
loading="lazy"
/>
<span className="text-sm font-medium">{skill.name}</span>
</div>
);
}
/**
* Marquee row component that scrolls skills horizontally
*/
function MarqueeRow({ skills, direction = "left", speed = 50 }: {
skills: SkillItem[];
direction?: "left" | "right";
speed?: number;
}) {
// Duplicate skills array to create seamless infinite scroll
const duplicatedSkills = [...skills, ...skills];
// State to control animation play/pause
const [isHovered, setIsHovered] = useState(false);
const controls = useAnimation();
// Start animation on component mount
useEffect(() => {
const startAnimation = () => {
controls.start({
x: direction === "left" ? ["-50%", "0%"] : ["0%", "-50%"],
transition: {
x: {
repeat: Infinity,
repeatType: "loop",
duration: speed,
ease: "linear"
}
}
});
};
startAnimation();
}, [controls, direction, speed]);
// Handle hover state changes
useEffect(() => {
if (isHovered) {
controls.stop();
} else {
controls.start({
x: direction === "left" ? ["-50%", "0%"] : ["0%", "-50%"],
transition: {
x: {
repeat: Infinity,
repeatType: "loop",
duration: speed,
ease: "linear"
}
}
});
}
}, [isHovered, controls, direction, speed]);
return (
<div
className="overflow-hidden"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<motion.div
className="flex gap-4 w-fit"
animate={controls}
>
{duplicatedSkills.map((skill, index) => (
<SkillItem key={`${skill.name}-${index}`} skill={skill} />
))}
</motion.div>
</div>
);
}
/**
* Main skills marquee component with multiple scrolling rows
*/
export default function SkillsMarquee({ lang }: { lang: "en" | "zh" }) {
const t = useTranslations(lang);
// Split skills into multiple rows for varied scrolling effect
const skillsRow1 = allSkills.slice(0, Math.ceil(allSkills.length / 3));
const skillsRow2 = allSkills.slice(Math.ceil(allSkills.length / 3), Math.ceil(allSkills.length * 2 / 3));
const skillsRow3 = allSkills.slice(Math.ceil(allSkills.length * 2 / 3));
return (
<section className="py-12 bg-gradient-to-b from-muted/20 to-background overflow-hidden">
<div className="container max-w-6xl mx-auto px-6 md:px-4">
<div className="relative">
<div className="space-y-6">
{/* First row - scrolling left */}
<MarqueeRow
skills={skillsRow1}
direction="left"
speed={60}
/>
{/* Second row - scrolling right */}
<MarqueeRow
skills={skillsRow2}
direction="right"
speed={45}
/>
{/* Third row - scrolling left */}
<MarqueeRow
skills={skillsRow3}
direction="left"
speed={55}
/>
</div>
{/* Gradient overlays for smooth fade effect */}
<div className="absolute left-0 top-0 bottom-0 w-20 bg-gradient-to-r from-background to-transparent pointer-events-none z-10" />
<div className="absolute right-0 top-0 bottom-0 w-20 bg-gradient-to-l from-background to-transparent pointer-events-none z-10" />
</div>
</div>
</section>
);
}