Files
zhaoguiyang.site/src/components/SkillsMarquee.tsx
joyzhao a8c3d4b197 refactor(i18n): consolidate Lang type imports to types/i18n
Move all Lang type imports from various locations to a centralized location in types/i18n for better maintainability and consistency
2025-06-21 09:36:32 +08:00

161 lines
5.5 KiB
TypeScript

import { motion, type Variants } from "framer-motion";
import { useTranslations } from "@/i18n/utils";
import { type SkillItem } from "@/types";
const allSkills: SkillItem[] = [
// Programming Languages
{ name: "TypeScript", icon: "typescript" },
{ name: "JavaScript", icon: "javascript" },
{ name: "React", icon: "react" },
{ name: "Vue", icon: "vue" },
// 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: "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
* Optimized for performance with constant animation
*/
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];
// Define animation variants to ensure type safety with hardware acceleration
const variants: Variants = {
animate: {
transform: direction === "left" ? ["translateX(0%)", "translateX(-50%)"] : ["translateX(-50%)", "translateX(0%)"],
transition: {
transform: {
repeat: Infinity,
repeatType: "loop" as const,
duration: speed,
ease: [0, 0, 1, 1] // 使用数组形式的 linear easing
}
}
}
};
return (
<div className="overflow-hidden">
<motion.div
className="flex gap-4 w-fit"
variants={variants}
animate="animate"
style={{
willChange: "transform",
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
WebkitFontSmoothing: "subpixel-antialiased"
}}
>
{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>
);
}