All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m36s
93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
import { useSignal } from "@preact/signals";
|
|
import { useEffect } from "preact/hooks";
|
|
|
|
interface Skill {
|
|
id: string;
|
|
name: string;
|
|
level: number;
|
|
}
|
|
|
|
interface ResumeSkillsProps {
|
|
skills: Skill[];
|
|
}
|
|
|
|
export default function ResumeSkills({ 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;
|
|
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="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;
|
|
|
|
return (
|
|
<div key={skill.id}>
|
|
<div class="flex justify-between items-center p-1 sm:p-2">
|
|
<span class="text-sm sm:text-base font-medium">
|
|
{skill.name}
|
|
</span>
|
|
<span class="text-xs sm:text-sm text-base-content/70">
|
|
{Math.round(currentLevel)}/5
|
|
</span>
|
|
</div>
|
|
<progress
|
|
class="progress progress-primary w-full h-2 sm:h-3 min-h-2 transition-all duration-100 ease-out"
|
|
value={progressValue}
|
|
max="100"
|
|
aria-label={`${skill.name} skill level: ${Math.round(currentLevel)} out of 5`}
|
|
></progress>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|