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:
198
src/components/SkillsMarquee.tsx
Normal file
198
src/components/SkillsMarquee.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user