Move all Lang type imports from various locations to a centralized location in types/i18n for better maintainability and consistency
161 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
} |