This commit is contained in:
95
src/components/ResumeSkills.tsx
Normal file
95
src/components/ResumeSkills.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { useSignal } from "@preact/signals";
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
interface Skill {
|
||||
id: string;
|
||||
name: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
interface ResumeSkillsProps {
|
||||
title: string;
|
||||
skills: Skill[];
|
||||
}
|
||||
|
||||
export default function ResumeSkills({ title, skills }: ResumeSkillsProps) {
|
||||
const animatedLevels = useSignal<{ [key: string]: number }>({});
|
||||
const hasAnimated = useSignal(false);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !hasAnimated.value) {
|
||||
hasAnimated.value = true;
|
||||
// Start animation for all skills
|
||||
skills.forEach((skill) => {
|
||||
animateSkill(skill.id, skill.level);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.3 },
|
||||
);
|
||||
|
||||
const skillsElement = document.getElementById("skills-section");
|
||||
if (skillsElement) {
|
||||
observer.observe(skillsElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (skillsElement) {
|
||||
observer.unobserve(skillsElement);
|
||||
}
|
||||
};
|
||||
}, [skills]);
|
||||
|
||||
const animateSkill = (skillId: string, targetLevel: number) => {
|
||||
const steps = 60;
|
||||
const increment = targetLevel / steps;
|
||||
let currentStep = 0;
|
||||
|
||||
const animate = () => {
|
||||
if (currentStep <= steps) {
|
||||
const currentValue = Math.min(increment * currentStep, targetLevel);
|
||||
animatedLevels.value = {
|
||||
...animatedLevels.value,
|
||||
[skillId]: currentValue,
|
||||
};
|
||||
currentStep++;
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="skills-section" class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
|
||||
<div class="card-body p-4 sm:p-6">
|
||||
<h2 class="card-title text-xl sm:text-2xl">{title || "Skills"}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4">
|
||||
{skills.map((skill) => {
|
||||
const currentLevel = animatedLevels.value[skill.id] || 0;
|
||||
const progressValue = currentLevel * 20; // Convert 1-5 scale to 0-100
|
||||
|
||||
return (
|
||||
<div key={skill.id}>
|
||||
<label class="label p-1 sm:p-2">
|
||||
<span class="label-text text-sm sm:text-base">
|
||||
{skill.name}
|
||||
</span>
|
||||
</label>
|
||||
<progress
|
||||
class="progress progress-primary w-full h-2 sm:h-3 transition-all duration-100 ease-out"
|
||||
value={progressValue}
|
||||
max="100"
|
||||
></progress>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user