diff --git a/package-lock.json b/package-lock.json index 17b3bda..7c422b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.2", + "react-tooltip": "^5.28.0", "styled-components": "^6.1.13", "vite-plugin-svgr": "^4.3.0" }, @@ -796,6 +797,28 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1818,6 +1841,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3837,6 +3865,19 @@ "react-dom": ">=18" } }, + "node_modules/react-tooltip": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz", + "integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", diff --git a/package.json b/package.json index 5caff60..86c8ef5 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^7.0.2", + "react-tooltip": "^5.28.0", "styled-components": "^6.1.13", "vite-plugin-svgr": "^4.3.0" }, diff --git a/src/components/TallContent.jsx b/src/components/TallContent.jsx index 6395bbd..876d5b8 100644 --- a/src/components/TallContent.jsx +++ b/src/components/TallContent.jsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useState, useEffect, useRef } from "react"; import "../css/TallContent.css"; import { useTheme } from "../Context/ThemeContext"; import { useLang } from "../Context/LangContext"; @@ -7,14 +7,105 @@ const TallContent = ({ initialAnimation, onMouseEnter, onAnimationEnd }) => { const [animation, setAnimation] = useState(initialAnimation); const { colormode } = useTheme(); const { language } = useLang(); + const canvasRef = useRef(null); + + const speedFactor = 0.2; // Geschwindigkeit anpassen: kleiner = langsamer, größer = schneller + + useEffect(() => { + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + + canvas.width = canvas.parentElement.offsetWidth; + canvas.height = canvas.parentElement.offsetHeight; + + const particles = Array.from({ length: 50 }, () => ({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + radius: Math.random() * 4 + 1, + dx: (Math.random() * 0.5 - 0.25) * speedFactor, + dy: (Math.random() * 0.5 - 0.25) * speedFactor, + color: `hsl(${Math.random() * 360}, 70%, 70%)`, + })); + + const starFieldEffect = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = "rgba(0, 0, 0, 0.1)"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + particles.forEach((particle) => { + particle.x += particle.dx; + particle.y += particle.dy; + + // Warp particles when they leave canvas + if (particle.x < 0) particle.x = canvas.width; + if (particle.x > canvas.width) particle.x = 0; + if (particle.y < 0) particle.y = canvas.height; + if (particle.y > canvas.height) particle.y = 0; + + ctx.beginPath(); + ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); + ctx.fillStyle = particle.color; + ctx.fill(); + }); + + requestAnimationFrame(starFieldEffect); + }; + + starFieldEffect(); + + canvas.addEventListener("mousemove", (e) => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + particles.forEach((particle) => { + const dx = particle.x - mouseX; + const dy = particle.y - mouseY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 150) { + const force = (150 - distance) / 150; + particle.dx += (dx / distance) * force * -0.05; + particle.dy += (dy / distance) * force * -0.05; + } + }); + }); + + canvas.addEventListener("click", (e) => { + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + particles.forEach((particle) => { + const dx = particle.x - mouseX; + const dy = particle.y - mouseY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 200) { + const force = (200 - distance) / 200; + particle.dx += (dx / distance) * force * 1.5; + particle.dy += (dy / distance) * force * 1.5; + } + }); + }); + + const handleResize = () => { + canvas.width = canvas.parentElement.offsetWidth; + canvas.height = canvas.parentElement.offsetHeight; + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, [speedFactor]); return (
onMouseEnter(setAnimation)} - onAnimationEnd={() => onAnimationEnd(setAnimation)} +/* onMouseEnter={() => onMouseEnter(setAnimation)} + */ onAnimationEnd={() => onAnimationEnd(setAnimation)} > - TallContent +
); }; diff --git a/src/components/TechStack.jsx b/src/components/TechStack.jsx index db918a4..7c4a124 100644 --- a/src/components/TechStack.jsx +++ b/src/components/TechStack.jsx @@ -17,16 +17,14 @@ import Linux from "../assets/icons/skill-icons/icons/Linux-Dark.svg?react"; import "../css/TechStack.css"; import { Link } from "react-router-dom"; -import "animate.css"; import { useTheme } from "../Context/ThemeContext"; import { useLang } from "../Context/LangContext"; const TechStack = ({ initialAnimation, - /* onMouseEnter, */ onAnimationEnd, isExpanded, - onExpandToggle, // Receive the handler as a prop + onExpandToggle, }) => { const techStack = [ { name: "React", component: ReactIcon, className: "reactIcon", id: 1 }, @@ -99,6 +97,7 @@ const TechStack = ({ const [animation, setAnimation] = useState(initialAnimation); const { colormode } = useTheme(); const { language } = useLang(); + return (
onMouseEnter(setAnimation)} */ onAnimationEnd={() => onAnimationEnd(setAnimation)} >

Tech Stack

{techStack - .filter((tech) => isExpanded || !tech.needExpand) // Filter based on isExpanded + .filter((tech) => isExpanded || !tech.needExpand) .map((tech) => { const IconComponent = tech.component; return ( -
+
+ {tech.name}
); })} diff --git a/src/css/Navigation.css b/src/css/Navigation.css index aede446..6a193f7 100644 --- a/src/css/Navigation.css +++ b/src/css/Navigation.css @@ -5,12 +5,17 @@ .navList { display: flex; flex-direction: column; - justify-content: space-around; + justify-content: start; align-items: center; + gap: 2.5rem; list-style-type: none; height: 100%; } +.navItem:last-child { + margin-top: auto; +} + .navItem { font-size: 1.5rem; width: 100%; diff --git a/src/css/TallContent.css b/src/css/TallContent.css index 8a28ade..f25c1e3 100644 --- a/src/css/TallContent.css +++ b/src/css/TallContent.css @@ -1,3 +1,12 @@ .tallContent { grid-area: 1 / 5 / 8 / 9; + position: relative; } + +.particleCanvas { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/src/css/TechStack.css b/src/css/TechStack.css index 2c0463f..4a54de3 100644 --- a/src/css/TechStack.css +++ b/src/css/TechStack.css @@ -72,3 +72,42 @@ .techStackContainer.expanded { max-height: 600px; /* Expanded max-height */ } + +.react-tooltip { + background-color: #333; + color: #fff; + font-size: 0.9rem; + border-radius: 4px; + padding: 5px 10px; +} + +/* Tooltip container */ +.tooltip { + position: relative; + display: inline-block; +} + +/* Tooltip text */ +.tooltipText { + visibility: hidden; + background-color: #333; + color: #fff; + text-align: center; + border-radius: 5px; + padding: 5px 10px; + position: absolute; + z-index: 1; + top: 90%; /* Position above the element */ + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 0.3s; + font-size: 0.8rem; + white-space: nowrap; +} + +/* Show the tooltip on hover */ +.tooltip:hover .tooltipText { + visibility: visible; + opacity: 1; +}