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:
194
src/components/AboutSection.tsx
Normal file
194
src/components/AboutSection.tsx
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslations } from "@/i18n/utils";
|
||||||
|
import MotionWrapper from "./MotionWrapper";
|
||||||
|
import { User, Code, Coffee, Heart } from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* About section component that displays personal introduction
|
||||||
|
*/
|
||||||
|
export default function AboutSection({ lang }: { lang: "en" | "zh" }) {
|
||||||
|
const t = useTranslations(lang);
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
delayChildren: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
number: "152",
|
||||||
|
label: t('about.stats.repositories'),
|
||||||
|
icon: Code,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "42K",
|
||||||
|
label: t('about.stats.commits'),
|
||||||
|
icon: Coffee,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: "87",
|
||||||
|
label: t('about.stats.prsmerged'),
|
||||||
|
icon: Heart,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="about" className="py-24 relative overflow-hidden">
|
||||||
|
{/* Background gradient */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/10 via-purple-800/5 to-indigo-700/10 dark:from-blue-900/20 dark:via-purple-800/10 dark:to-indigo-700/20"></div>
|
||||||
|
|
||||||
|
<div className="container max-w-6xl mx-auto px-6 md:px-4 relative z-10">
|
||||||
|
<MotionWrapper>
|
||||||
|
<motion.div
|
||||||
|
className="text-center mb-16"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="visible"
|
||||||
|
viewport={{ once: true, amount: 0.3 }}
|
||||||
|
>
|
||||||
|
{/* Section Title */}
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center justify-center mb-6"
|
||||||
|
variants={itemVariants}
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||||
|
{t('about.title')}
|
||||||
|
</h2>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* About Content */}
|
||||||
|
<div className="grid lg:grid-cols-2 gap-12 items-stretch">
|
||||||
|
{/* Left side - Description and Stats */}
|
||||||
|
<motion.div
|
||||||
|
className="bg-white/20 dark:bg-gray-800/50 backdrop-blur-sm border border-white/30 dark:border-gray-700/40 rounded-xl p-6 space-y-8"
|
||||||
|
variants={itemVariants}
|
||||||
|
>
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||||
|
<span className="mr-2">👋</span>
|
||||||
|
关于我
|
||||||
|
</h3>
|
||||||
|
<div className="prose prose-lg dark:prose-invert max-w-none">
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
|
{t('about.description.paragraph1')}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
|
{t('about.description.paragraph2')}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 leading-relaxed">
|
||||||
|
{t('about.description.paragraph3')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Grid */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{stats.map((stat, index) => {
|
||||||
|
const IconComponent = stat.icon;
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
className="text-center p-4 rounded-xl bg-white/30 dark:bg-gray-700/60 backdrop-blur-sm border border-white/40 dark:border-gray-600/50 hover:bg-white/40 dark:hover:bg-gray-700/80 transition-all duration-300"
|
||||||
|
variants={itemVariants}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<IconComponent className="h-8 w-8 mx-auto mb-2 text-blue-500" />
|
||||||
|
<div className="text-2xl font-bold text-gray-900 dark:text-white mb-1">
|
||||||
|
{stat.number}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
{stat.label}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Right side - Skills Toolbox */}
|
||||||
|
<motion.div
|
||||||
|
className="bg-white/20 dark:bg-gray-800/50 backdrop-blur-sm border border-white/30 dark:border-gray-700/40 rounded-xl p-6 flex flex-col"
|
||||||
|
variants={itemVariants}
|
||||||
|
>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-6 flex items-center">
|
||||||
|
<span className="mr-2">💻</span>
|
||||||
|
{t('about.toolbox.title')}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-6 flex-1">
|
||||||
|
{/* Skills Progress */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{t('about.toolbox.frontend')}</span>
|
||||||
|
<span className="px-2 py-1 text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 rounded">90%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<div className="bg-gradient-to-r from-blue-500 to-purple-500 h-2 rounded-full" style={{ width: '90%' }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{t('about.toolbox.backend')}</span>
|
||||||
|
<span className="px-2 py-1 text-xs bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300 rounded">85%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<div className="bg-gradient-to-r from-green-500 to-teal-500 h-2 rounded-full" style={{ width: '85%' }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{t('about.toolbox.devops')}</span>
|
||||||
|
<span className="px-2 py-1 text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 rounded">75%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<div className="bg-gradient-to-r from-purple-500 to-pink-500 h-2 rounded-full" style={{ width: '75%' }}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-700 dark:text-gray-300">{t('about.toolbox.mobile')}</span>
|
||||||
|
<span className="px-2 py-1 text-xs bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-300 rounded">65%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||||
|
<div className="bg-gradient-to-r from-orange-500 to-red-500 h-2 rounded-full" style={{ width: '65%' }}></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tech Stack Tags */}
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{['JavaScript', 'React', 'Node.js', 'TypeScript', 'TailwindCSS', 'Python', 'Docker', 'Git'].map((tech) => (
|
||||||
|
<span
|
||||||
|
key={tech}
|
||||||
|
className="px-3 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</MotionWrapper>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { workExperience } from "@/lib/data";
|
|
||||||
import TimelineItem from "./TimelineItem";
|
|
||||||
import { useTranslations } from "@/i18n/utils";
|
|
||||||
import { Briefcase } from "lucide-react";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import MotionWrapper from "./MotionWrapper";
|
|
||||||
|
|
||||||
export default function ExperienceSection({ lang }: { lang: "en" | "zh" }) {
|
|
||||||
const t = useTranslations(lang);
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
id="experience"
|
|
||||||
className="py-12 bg-gradient-to-b from-muted/20 to-background"
|
|
||||||
>
|
|
||||||
<div className="container max-w-4xl mx-auto px-6 md:px-4">
|
|
||||||
<MotionWrapper>
|
|
||||||
<h2 className="text-2xl font-bold mb-8 text-center md:text-left flex items-center md:inline-block">
|
|
||||||
<motion.span
|
|
||||||
className="inline-block mr-2"
|
|
||||||
initial={{ rotate: 0 }}
|
|
||||||
whileInView={{ rotate: [0, -10, 10, -5, 5, 0] }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
>
|
|
||||||
💼
|
|
||||||
</motion.span>
|
|
||||||
{t('experience.title')}
|
|
||||||
</h2>
|
|
||||||
</MotionWrapper>
|
|
||||||
<div className="mb-8">
|
|
||||||
{workExperience.map((job, index) => {
|
|
||||||
const companyKey = job.company.toLowerCase().replace(/ /g, '');
|
|
||||||
return (
|
|
||||||
<TimelineItem
|
|
||||||
key={job.company + job.period}
|
|
||||||
title={`👨💻 ${t(`work.${companyKey}.position` as any)} | ${t(`work.${companyKey}.company` as any)}`}
|
|
||||||
subtitle={`🌍 ${t(`work.${companyKey}.location` as any)}`}
|
|
||||||
date={`📅 ${t(`work.${companyKey}.period` as any)}`}
|
|
||||||
isLast={index === workExperience.length - 1}
|
|
||||||
index={index}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
className="mt-3 p-4 bg-background/80 backdrop-blur-sm backdrop-filter rounded-lg border border-purple-500/20 dark:bg-card/10 dark:border-purple-500/10 shadow-sm"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.2 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
>
|
|
||||||
<div className="flex items-center mb-3">
|
|
||||||
<div className="h-6 w-6 flex items-center justify-center rounded-full bg-purple-500/10 mr-2">
|
|
||||||
<Briefcase className="h-4 w-4 text-purple-500" />
|
|
||||||
</div>
|
|
||||||
<h4 className="text-sm font-medium">{t('experience.keyAchievements')}</h4>
|
|
||||||
</div>
|
|
||||||
<ul className="list-none ml-4 space-y-2 text-sm">
|
|
||||||
{job.achievements.map((achievement, i) => (
|
|
||||||
<motion.li
|
|
||||||
key={i}
|
|
||||||
className="text-muted-foreground relative pl-6"
|
|
||||||
initial={{ opacity: 0, x: -10 }}
|
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ duration: 0.3, delay: 0.1 * i }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
>
|
|
||||||
{t(`work.${companyKey}.achievements.${i}` as any)}
|
|
||||||
</motion.li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</motion.div>
|
|
||||||
</TimelineItem>
|
|
||||||
)})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -41,7 +41,7 @@ export default function GlassHeader({ lang }: GlassHeaderProps) {
|
|||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
|
<nav className="hidden md:flex items-center space-x-6 text-sm font-medium">
|
||||||
{[
|
{[
|
||||||
{ key: 'nav.skills', icon: '🛠️ ', sectionId: 'skills' },
|
{ key: 'nav.about', icon: '👨💻 ', sectionId: 'about' },
|
||||||
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
|
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<a
|
<a
|
||||||
@@ -82,7 +82,7 @@ export default function GlassHeader({ lang }: GlassHeaderProps) {
|
|||||||
}`}>
|
}`}>
|
||||||
<nav className="flex flex-col space-y-3 text-sm font-medium">
|
<nav className="flex flex-col space-y-3 text-sm font-medium">
|
||||||
{[
|
{[
|
||||||
{ key: 'nav.skills', icon: '🛠️ ', sectionId: 'skills' },
|
{ key: 'nav.about', icon: '👨💻 ', sectionId: 'about' },
|
||||||
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
|
{ key: 'nav.projects', icon: '🚀 ', sectionId: 'projects' },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<a
|
<a
|
||||||
|
|||||||
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { skills } from "@/lib/data";
|
|
||||||
import { useTranslations } from "@/i18n/utils";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import MotionWrapper from "./MotionWrapper";
|
|
||||||
import { GlassCard } from "./ui/glass-card";
|
|
||||||
|
|
||||||
function SkillTag({ skill, index }: { skill: string; index: number }) {
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0, scale: 0.8 }}
|
|
||||||
whileInView={{ opacity: 1, scale: 1 }}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 260,
|
|
||||||
damping: 20,
|
|
||||||
delay: 0.05 * index,
|
|
||||||
}}
|
|
||||||
whileHover={{ scale: 1.05, y: -2 }}
|
|
||||||
className="px-3 py-1 bg-muted/80 backdrop-blur-sm rounded-md text-sm border border-purple-500/10 shadow-sm"
|
|
||||||
>
|
|
||||||
{skill}
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerVariants = {
|
|
||||||
hidden: { opacity: 0 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const skillCategoryVariants = {
|
|
||||||
hidden: { opacity: 0, y: 20 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
duration: 0.5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SkillsSection({ lang }: { lang: "en" | "zh" }) {
|
|
||||||
const t = useTranslations(lang);
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
id="skills"
|
|
||||||
className="py-12 bg-gradient-to-b from-background to-muted/20"
|
|
||||||
>
|
|
||||||
<div className="container max-w-4xl mx-auto px-6 md:px-4">
|
|
||||||
<MotionWrapper>
|
|
||||||
<h2 className="text-2xl font-bold mb-8 text-center md:text-left">
|
|
||||||
🛠️ {t('skills.title')}
|
|
||||||
</h2>
|
|
||||||
</MotionWrapper>
|
|
||||||
|
|
||||||
<motion.div
|
|
||||||
className="space-y-6"
|
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
>
|
|
||||||
<motion.div variants={skillCategoryVariants}>
|
|
||||||
<GlassCard className="p-4">
|
|
||||||
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
|
|
||||||
<span className="mr-2 text-xl">💻</span> {t('skills.programmingLanguages')}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
|
||||||
{skills.programmingLanguages.map((skill, index) => (
|
|
||||||
<SkillTag key={skill} skill={skill} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={skillCategoryVariants}>
|
|
||||||
<GlassCard className="p-4">
|
|
||||||
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
|
|
||||||
<span className="mr-2 text-xl">🎨</span> {t('skills.frontendDevelopment')}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
|
||||||
{skills.frontendDevelopment.map((skill, index) => (
|
|
||||||
<SkillTag key={skill} skill={skill} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={skillCategoryVariants}>
|
|
||||||
<GlassCard className="p-4">
|
|
||||||
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
|
|
||||||
<span className="mr-2 text-xl">⚙️</span> {t('skills.backendDevelopment')}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
|
||||||
{skills.backendDevelopment.map((skill, index) => (
|
|
||||||
<SkillTag key={skill} skill={skill} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={skillCategoryVariants}>
|
|
||||||
<GlassCard className="p-4">
|
|
||||||
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
|
|
||||||
<span className="mr-2 text-xl">🗄️</span> {t('skills.databaseAndStorage')}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
|
||||||
{skills.databaseAndStorage.map((skill, index) => (
|
|
||||||
<SkillTag key={skill} skill={skill} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={skillCategoryVariants}>
|
|
||||||
<GlassCard className="p-4">
|
|
||||||
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
|
|
||||||
<span className="mr-2 text-xl">☁️</span> {t('skills.cloudAndDevOps')}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
|
||||||
{skills.cloudAndDevOps.map((skill, index) => (
|
|
||||||
<SkillTag key={skill} skill={skill} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div variants={skillCategoryVariants}>
|
|
||||||
<GlassCard className="p-4">
|
|
||||||
<h3 className="text-lg font-medium mb-3 text-center md:text-left flex items-center">
|
|
||||||
<span className="mr-2 text-xl">🧰</span> {t('skills.toolsAndServices')}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
|
||||||
{skills.toolsAndServices.map((skill, index) => (
|
|
||||||
<SkillTag key={skill} skill={skill} index={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</GlassCard>
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
|
|
||||||
interface TimelineItemProps {
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
date: string;
|
|
||||||
isLast?: boolean;
|
|
||||||
index?: number;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TimelineItem({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
date,
|
|
||||||
isLast = false,
|
|
||||||
index = 0,
|
|
||||||
children,
|
|
||||||
}: TimelineItemProps) {
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
className="relative flex gap-6"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.2 }}
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<motion.div
|
|
||||||
className="flex h-[18px] w-[18px] rounded-full border border-purple-500/50 bg-background dark:bg-muted z-10"
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
whileInView={{ scale: 1 }}
|
|
||||||
transition={{
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 300,
|
|
||||||
damping: 15,
|
|
||||||
delay: index * 0.2 + 0.2,
|
|
||||||
}}
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
/>
|
|
||||||
{!isLast && (
|
|
||||||
<motion.div
|
|
||||||
className="w-px grow bg-gradient-to-b from-purple-500/50 to-pink-500/30 dark:from-purple-500/30 dark:to-pink-500/10"
|
|
||||||
initial={{ height: 0 }}
|
|
||||||
whileInView={{ height: "100%" }}
|
|
||||||
transition={{ duration: 0.8, delay: index * 0.2 + 0.3 }}
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={cn("pb-8", isLast ? "pb-0" : "")}>
|
|
||||||
<motion.div
|
|
||||||
className="flex flex-col gap-0.5"
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
|
||||||
whileInView={{ opacity: 1, x: 0 }}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.2 + 0.1 }}
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
>
|
|
||||||
<h3 className="font-medium">{title}</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">{subtitle}</p>
|
|
||||||
<p className="text-xs text-muted-foreground/70 mb-2">{date}</p>
|
|
||||||
</motion.div>
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
whileInView={{ opacity: 1 }}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.2 + 0.4 }}
|
|
||||||
viewport={{ once: true, margin: "-50px" }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -9,11 +9,8 @@ export const defaultLang = 'en';
|
|||||||
export const ui = {
|
export const ui = {
|
||||||
en: {
|
en: {
|
||||||
'nav.home': 'Home',
|
'nav.home': 'Home',
|
||||||
|
'nav.about': 'About Me',
|
||||||
'nav.projects': 'Projects',
|
'nav.projects': 'Projects',
|
||||||
'nav.experience': 'Experience',
|
|
||||||
'nav.skills': 'Skills',
|
|
||||||
'nav.awards': 'Awards',
|
|
||||||
'nav.education': 'Education',
|
|
||||||
'footer.rights': 'All rights reserved.',
|
'footer.rights': 'All rights reserved.',
|
||||||
'site.title': 'My Portfolio',
|
'site.title': 'My Portfolio',
|
||||||
'page.home.title': 'Home',
|
'page.home.title': 'Home',
|
||||||
@@ -40,6 +37,20 @@ export const ui = {
|
|||||||
// Skills Section
|
// Skills Section
|
||||||
'skills.title': 'Skills',
|
'skills.title': 'Skills',
|
||||||
|
|
||||||
|
// About Section
|
||||||
|
'about.title': 'About Me',
|
||||||
|
'about.description.paragraph1': 'I\'m a passionate developer with 5+ years of experience building web applications and contributing to open source projects. I specialize in creating clean, efficient, and maintainable code.',
|
||||||
|
'about.description.paragraph2': 'When I\'m not coding, you can find me exploring new technologies, writing tech articles, or enjoying a fresh cup of coffee while debugging complex problems.',
|
||||||
|
'about.description.paragraph3': 'I believe in continuous learning and staying up-to-date with the latest industry trends and best practices.',
|
||||||
|
'about.stats.repositories': 'Repositories',
|
||||||
|
'about.stats.commits': 'Commits',
|
||||||
|
'about.stats.prsmerged': 'PRs Merged',
|
||||||
|
'about.toolbox.title': 'My Toolbox',
|
||||||
|
'about.toolbox.frontend': 'Frontend',
|
||||||
|
'about.toolbox.backend': 'Backend',
|
||||||
|
'about.toolbox.devops': 'DevOps',
|
||||||
|
'about.toolbox.mobile': 'Mobile',
|
||||||
|
|
||||||
// Projects Section
|
// Projects Section
|
||||||
'projects.title': 'Latest Projects',
|
'projects.title': 'Latest Projects',
|
||||||
'projects.viewOnGithub': 'View on GitHub',
|
'projects.viewOnGithub': 'View on GitHub',
|
||||||
@@ -168,10 +179,7 @@ export const ui = {
|
|||||||
zh: {
|
zh: {
|
||||||
'nav.home': '首页',
|
'nav.home': '首页',
|
||||||
'nav.projects': '项目经历',
|
'nav.projects': '项目经历',
|
||||||
'nav.experience': '工作经历',
|
'nav.about': '关于我',
|
||||||
'nav.skills': '专业技能',
|
|
||||||
'nav.awards': '奖项荣誉',
|
|
||||||
'nav.education': '教育背景',
|
|
||||||
'footer.rights': '版权所有。',
|
'footer.rights': '版权所有。',
|
||||||
'site.title': '我的作品集',
|
'site.title': '我的作品集',
|
||||||
'page.home.title': '首页',
|
'page.home.title': '首页',
|
||||||
@@ -198,6 +206,20 @@ export const ui = {
|
|||||||
// Skills Section
|
// Skills Section
|
||||||
'skills.title': '专业技能',
|
'skills.title': '专业技能',
|
||||||
|
|
||||||
|
// About Section
|
||||||
|
'about.title': '关于我',
|
||||||
|
'about.description.paragraph1': '我是一名充满热情的开发者,拥有5年以上构建Web应用程序和参与开源项目的经验。我专注于创建简洁、高效且易于维护的代码。',
|
||||||
|
'about.description.paragraph2': '当我不在编程时,你可以发现我在探索新技术、撰写技术文章,或者在调试复杂问题时享受一杯新鲜的咖啡。',
|
||||||
|
'about.description.paragraph3': '我相信持续学习,并与最新的行业趋势和最佳实践保持同步。',
|
||||||
|
'about.stats.repositories': '代码仓库',
|
||||||
|
'about.stats.commits': '提交次数',
|
||||||
|
'about.stats.prsmerged': '合并请求',
|
||||||
|
'about.toolbox.title': '我的工具箱',
|
||||||
|
'about.toolbox.frontend': '前端',
|
||||||
|
'about.toolbox.backend': '后端',
|
||||||
|
'about.toolbox.devops': 'DevOps',
|
||||||
|
'about.toolbox.mobile': '移动端',
|
||||||
|
|
||||||
// Projects Section
|
// Projects Section
|
||||||
'projects.title': '最新项目',
|
'projects.title': '最新项目',
|
||||||
'projects.viewOnGithub': '在 GitHub 上查看',
|
'projects.viewOnGithub': '在 GitHub 上查看',
|
||||||
|
|||||||
@@ -6,36 +6,6 @@ export const personalInfo = {
|
|||||||
linkedin: "https://www.linkedin.com/in/rishikeshs/",
|
linkedin: "https://www.linkedin.com/in/rishikeshs/",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const skills = {
|
|
||||||
programmingLanguages: [
|
|
||||||
"TypeScript",
|
|
||||||
"JavaScript",
|
|
||||||
"React",
|
|
||||||
"Vue",
|
|
||||||
"微信小程序",
|
|
||||||
"UniApp",
|
|
||||||
],
|
|
||||||
frontendDevelopment: [
|
|
||||||
"NuxtJs",
|
|
||||||
"NextJs",
|
|
||||||
"React Native",
|
|
||||||
"Shadcn UI",
|
|
||||||
"PrimeVue",
|
|
||||||
"Naive-UI",
|
|
||||||
"Tailwind CSS",
|
|
||||||
],
|
|
||||||
backendDevelopment: ["NodeJs", "ExpressJs", "NestJs", "FastifyJs", "HonoJs"],
|
|
||||||
databaseAndStorage: ["PostgreSQL", "MongoDB", "Drizzle (ORM)", "Mongoose", "Prisma"],
|
|
||||||
cloudAndDevOps: ["AWS", "Cloudflare", "Vercel"],
|
|
||||||
toolsAndServices: [
|
|
||||||
"Zod",
|
|
||||||
"BetterAuth",
|
|
||||||
"Clerk (Auth)",
|
|
||||||
"GitLab",
|
|
||||||
"CI/CD"
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const projects = [
|
export const projects = [
|
||||||
{
|
{
|
||||||
title: "projects.taskify.title",
|
title: "projects.taskify.title",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import GlassHeader from "@/components/GlassHeader";
|
import GlassHeader from "@/components/GlassHeader";
|
||||||
import HeroSection from "@/components/HeroSection";
|
import HeroSection from "@/components/HeroSection";
|
||||||
import SkillsSection from "@/components/SkillsSection";
|
import AboutSection from "@/components/AboutSection";
|
||||||
|
import SkillsMarquee from "@/components/SkillsMarquee";
|
||||||
import ProjectsSection from "@/components/ProjectsSection";
|
import ProjectsSection from "@/components/ProjectsSection";
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import { useTranslations, type Lang } from "@/i18n/utils";
|
import { useTranslations, type Lang } from "@/i18n/utils";
|
||||||
@@ -17,7 +18,8 @@ const pageTitle = t('page.home.title');
|
|||||||
<GlassHeader lang={lang} client:only="react" />
|
<GlassHeader lang={lang} client:only="react" />
|
||||||
<main class="min-h-screen">
|
<main class="min-h-screen">
|
||||||
<HeroSection lang={lang} client:only="react" />
|
<HeroSection lang={lang} client:only="react" />
|
||||||
<SkillsSection lang={lang} client:only="react" />
|
<SkillsMarquee lang={lang} client:only="react" />
|
||||||
|
<AboutSection lang={lang} client:only="react" />
|
||||||
<ProjectsSection lang={lang} client:only="react" />
|
<ProjectsSection lang={lang} client:only="react" />
|
||||||
</main>
|
</main>
|
||||||
<Footer lang={lang} client:only="react" />
|
<Footer lang={lang} client:only="react" />
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import Layout from "@/layouts/Layout.astro";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
import GlassHeader from "@/components/GlassHeader.tsx";
|
import GlassHeader from "@/components/GlassHeader.tsx";
|
||||||
import HeroSection from "@/components/HeroSection.tsx";
|
import HeroSection from "@/components/HeroSection.tsx";
|
||||||
import SkillsSection from "@/components/SkillsSection.tsx";
|
import AboutSection from "@/components/AboutSection.tsx";
|
||||||
|
import SkillsMarquee from "@/components/SkillsMarquee.tsx";
|
||||||
import ProjectsSection from "@/components/ProjectsSection.tsx";
|
import ProjectsSection from "@/components/ProjectsSection.tsx";
|
||||||
import Footer from "@/components/Footer.tsx";
|
import Footer from "@/components/Footer.tsx";
|
||||||
import { useTranslations } from "@/i18n/utils";
|
import { useTranslations } from "@/i18n/utils";
|
||||||
@@ -16,7 +17,8 @@ const pageTitle = t('page.home.title');
|
|||||||
<GlassHeader lang={lang} client:only="react" />
|
<GlassHeader lang={lang} client:only="react" />
|
||||||
<main class="min-h-screen">
|
<main class="min-h-screen">
|
||||||
<HeroSection lang={lang} client:only="react" />
|
<HeroSection lang={lang} client:only="react" />
|
||||||
<SkillsSection lang={lang} client:only="react" />
|
<SkillsMarquee lang={lang} client:only="react" />
|
||||||
|
<AboutSection lang={lang} client:only="react" />
|
||||||
<ProjectsSection lang={lang} client:only="react" />
|
<ProjectsSection lang={lang} client:only="react" />
|
||||||
</main>
|
</main>
|
||||||
<Footer lang={lang} client:load />
|
<Footer lang={lang} client:load />
|
||||||
|
|||||||
Reference in New Issue
Block a user