diff --git a/package-lock.json b/package-lock.json index 3d039e6..4e7b11a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "portfolio", "version": "0.1.0", "dependencies": { + "framer-motion": "^12.6.2", "next": "15.2.0", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -2745,6 +2746,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.6.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.2.tgz", + "integrity": "sha512-7LgPRlPs5aG8UxeZiMCMZz8firC53+2+9TnWV22tuSi38D3IFRxHRUqOREKckAkt6ztX+Dn6weLcatQilJTMcg==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.6.1", + "motion-utils": "^12.5.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3955,6 +3983,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.1.tgz", + "integrity": "sha512-8XVsriTUEVOepoIDgE/LDGdg7qaKXWdt+wQA/8z0p8YzJDLYL8gbimZ3YkCLlj7bB2i/4UBD/g+VO7y9ZY0zHQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.5.0" + } + }, + "node_modules/motion-utils": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.5.0.tgz", + "integrity": "sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 7d2a1e2..dd29cdf 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,16 @@ "lint": "next lint" }, "dependencies": { + "framer-motion": "^12.6.2", + "next": "15.2.0", "react": "^19.0.0", - "react-dom": "^19.0.0", - "next": "15.2.0" + "react-dom": "^19.0.0" }, "devDependencies": { + "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.2.0", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4" } } diff --git a/src/app/about/page.js b/src/app/about/page.js index 0cded19..854f41e 100644 --- a/src/app/about/page.js +++ b/src/app/about/page.js @@ -9,85 +9,76 @@ export default function About() { content: "I discovered my passion for technology by experimenting with Scratch and creating some simple Games." }, { - date: "2022", - title: "Exploring Web Development", - content: "Started creating my first website and got into HTML and CSS." + date: "2020", + title: "Exploring Web & Scripting", + content: "Started creating small websites and got into JavaScript, CSS, and working with various scripting languages." }, { - date: "2023", - title: "Diving into Informatics", - content: "Started with Python and basic exploration of 'real' programming." + date: "2022", + title: "Diving into Game Development", + content: "Tried Unity and C#, building small 2D and 3D prototypes to expand my skill set." }, { date: "2024", title: "Full Stack & Beyond", - content: "Ventured into backend (SQL, Python) and frontend using Javascript" + content: "Ventured into backend (SQL, Python) and continued refining my frontend abilities, always exploring new challenges." } ]; + const skills = [ + { name: "JavaScript", level: 80, waveSpeed: "2s" }, + { name: "Python", level: 75, waveSpeed: "2.5s" }, + { name: "C#", level: 50, waveSpeed: "3s" }, + { name: "SQL", level: 85, waveSpeed: "2.2s" }, + { name: "HTML & CSS", level: 70, waveSpeed: "2.7s" }, + ]; + const sectionRefs = timelineData.map(() => useRef(null)); const aboutMeRef = useRef(null); - const [currentSection, setCurrentSection] = useState(0); - const [timelineFinished, setTimelineFinished] = useState(false); - const [isManualScroll, setIsManualScroll] = useState(false); // Track user scrolling const scrollContainerRef = useRef(null); + const [currentSection, setCurrentSection] = useState(0); + const [timelineFinished, setTimelineFinished] = useState(false); + + // Scroll to 'About Me' title and reset timeline on page load useEffect(() => { setCurrentSection(0); // Reset timeline setTimelineFinished(false); - // Ensure 'About Me' is visible on reload setTimeout(() => { - aboutMeRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + aboutMeRef.current?.scrollIntoView({behavior: "smooth", block: "start"}); }, 100); }, []); // Handle button navigation const handleButtonClick = (direction) => { - setIsManualScroll(false); // Ensure programmatic scrolling happens setCurrentSection((prev) => { const newSection = Math.max(0, Math.min(prev + direction, timelineData.length - 1)); if (newSection === timelineData.length - 1) { setTimelineFinished(true); + } else { + setTimelineFinished(false); } + sectionRefs[newSection]?.current?.scrollIntoView({behavior: "smooth", inline: "start"}); return newSection; }); }; - // Scroll to current section (only if not manually scrolling) - useEffect(() => { - if (!isManualScroll) { - sectionRefs[currentSection]?.current?.scrollIntoView({behavior: "smooth", inline: "start"}); - } - }, [currentSection, isManualScroll]); - useEffect(() => { - const handleScroll = () => { - setIsManualScroll(true); - }; - - const scrollContainer = scrollContainerRef.current; - if (scrollContainer) { - scrollContainer.addEventListener("scroll", handleScroll); - } - - return () => { - if (scrollContainer) { - scrollContainer.removeEventListener("scroll", handleScroll); - } - }; - }, []); - - // Intersection Observer to detect when last section is fully visible + // Track scrolling manually and update currentSection useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { - if (entry.isIntersecting && entry.target === sectionRefs[timelineData.length - 1].current) { - setTimelineFinished(true); + if (entry.isIntersecting) { + const index = sectionRefs.findIndex(ref => ref.current === entry.target); + if (index !== -1) { + setCurrentSection(index); + setTimelineFinished(index === timelineData.length - 1); + } } }); }, - {threshold: 0.8} // Increased threshold to ensure full visibility before triggering + {threshold: 0.6} ); sectionRefs.forEach((ref) => { @@ -115,7 +106,7 @@ export default function About() {

About Me

I’m a passionate developer always excited about new challenges. - Scroll down to check out my coding journey through the years! + Scroll down to learn more about me!

@@ -195,26 +186,31 @@ export default function About() { + {/* Skills Section with Liquid Progress Bars */} {timelineFinished && ( -
-

My Skills

-

- By completing projects in web development, scripting, and game - design, I’ve cultivated a diverse set of skills — spanning - JavaScript, Python, C#, and more! -

+
+

My Skills

+ +
+ {skills.map((skill, index) => ( +
+
+

{skill.level}%

+
+

{skill.name}

+
+ ))} +
)} - ); + ) } \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index f6e51d9..5b7db43 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -181,3 +181,15 @@ body { object-fit: contain; display: block; } + +@keyframes wave { + 0% { + transform: translateY(8px); + } + 50% { + transform: translateY(0px); + } + 100% { + transform: translateY(8px); + } +}