Newer
Older
My-Portfolio / frontend / src / components / sections / projects / HoverableSkill.jsx
  1. import React, { useEffect, useRef, useState } from "react";
  2. import { motion, useAnimation } from "framer-motion";
  3. import clsx from "clsx";
  4. import MouseTracker from "../../MouseTracker"; // Import the MouseTracker component
  5. const HoverableSkillList = ({
  6. title,
  7. items = [], // Default to an empty array to avoid undefined errors
  8. color = "red",
  9. hoveredItem,
  10. setHoveredItem,
  11. section,
  12. getKey,
  13. isVisible,
  14. }) => {
  15. const borderClass = `text-${color}-400`;
  16. const controls = useAnimation();
  17. const skillLevelRefs = useRef([]);
  18. const [isMobile, setIsMobile] = useState(false);
  19. // Detect if the device is mobile
  20. useEffect(() => {
  21. const checkIsMobile = () => {
  22. setIsMobile(window.innerWidth <= 768); // Adjust breakpoint as needed
  23. };
  24. checkIsMobile(); // Check on initial render
  25. window.addEventListener("resize", checkIsMobile); // Check on window resize
  26. return () => {
  27. window.removeEventListener("resize", checkIsMobile); // Cleanup
  28. };
  29. }, []);
  30. useEffect(() => {
  31. if (isVisible) {
  32. controls.start((item) => {
  33. const circumference = 2 * Math.PI * 40; // Adjusted radius
  34. const skillLevel = parseInt(
  35. item.skill.split(": ")[1].split("/")[0],
  36. 10
  37. );
  38. return {
  39. strokeDashoffset: circumference - (skillLevel / 5) * circumference,
  40. transition: { duration: 1, ease: "easeInOut" },
  41. };
  42. });
  43. } else {
  44. controls.set({ strokeDashoffset: 2 * Math.PI * 40 });
  45. }
  46. }, [isVisible, controls]);
  47. // Check if the current item is hovered
  48. const isHovered =
  49. hoveredItem.section === section && hoveredItem.index !== null;
  50. return (
  51. <div className="relative mx-auto text-center">
  52. <h2
  53. className={clsx("text-2xl font-sans font-semibold mb-8", borderClass)}
  54. >
  55. {title}
  56. </h2>
  57. <div className="flex gap-3 flex-wrap justify-center">
  58. {items.map((item, index) => {
  59. const circumference = 2 * Math.PI * 40;
  60. return (
  61. <div
  62. key={getKey(item, index)}
  63. className="relative flex flex-col items-center mb-2 group"
  64. onMouseEnter={() =>
  65. !isMobile && setHoveredItem({ section, index })
  66. }
  67. onMouseLeave={() =>
  68. !isMobile && setHoveredItem({ section: null, index: null })
  69. }
  70. onClick={() => isMobile && setHoveredItem({ section, index })}
  71. >
  72. <div className="relative w-[70px] h-[70px] sm:w-[70px] sm:h-[70px] md:w-[80px] md:h-[80px] lg:w-[90px] lg:h-[90px]">
  73. {" "}
  74. {/* Adjusted size for mobile */}
  75. <svg
  76. className="absolute top-0 left-0 w-full h-full transform -rotate-90"
  77. viewBox="0 0 100 100"
  78. >
  79. {/* Background Circle */}
  80. <circle
  81. cx="50"
  82. cy="50"
  83. r="40"
  84. fill="transparent"
  85. stroke="gray"
  86. strokeWidth="6"
  87. strokeDasharray={circumference}
  88. />
  89. {/* Animated Progress Circle */}
  90. <motion.circle
  91. cx="50"
  92. cy="50"
  93. r="40"
  94. fill="transparent"
  95. stroke="currentColor"
  96. strokeWidth="7"
  97. strokeDasharray={circumference}
  98. initial={{ strokeDashoffset: circumference }}
  99. animate={controls}
  100. custom={item}
  101. className={clsx(borderClass, "transition-all duration-300")}
  102. />
  103. </svg>
  104. {/* Icon */}
  105. <motion.img
  106. alt={item.name}
  107. src={item.logo}
  108. className={clsx(
  109. "p-2 w-[60px] h-[60px] sm:w-[70px] sm:h-[70px] rounded-full transition-all duration-100 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2",
  110. isHovered &&
  111. hoveredItem.index === index &&
  112. "shadow-[0_0_50px_rgba(255,255,255,0.9)]"
  113. )}
  114. />
  115. </div>
  116. <p className="mt-2 text-base text-white">{item.name}</p>
  117. {/* Inline Tooltip for Mobile */}
  118. {isMobile && isHovered && hoveredItem.index === index && (
  119. <div className="absolute bottom-full mb-2 bg-black text-white text-sm font-bold rounded-md px-8 py-3 shadow-lg">
  120. {item.skill}
  121. </div>
  122. )}
  123. </div>
  124. );
  125. })}
  126. </div>
  127. {/* MouseTracker for Desktop Tooltip */}
  128. {!isMobile && isHovered && (
  129. <MouseTracker text={items[hoveredItem.index].skill} />
  130. )}
  131. </div>
  132. );
  133. };
  134. export default HoverableSkillList;