From 3251d5d3ee44d32b6e1eb5b7cd0bac0c84eba755 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:55:34 +0000 Subject: [PATCH 1/9] Initial plan From da9bfbaec3bbd5d455b322f2f394fc45a346b5c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:02:37 +0000 Subject: [PATCH 2/9] Performance optimizations: Fix redundant loops, add React.memo, and optimize re-renders Co-authored-by: Keyyard <84187238+Keyyard@users.noreply.github.com> --- eslint.config.js | 6 ++---- src/components/layout/Background.tsx | 8 ++++++-- src/components/layout/MyHead.tsx | 20 ++++++++------------ src/components/layout/Nav.tsx | 4 +++- src/components/sections/Hero.tsx | 21 +++++++++++++-------- src/components/sections/Projects.tsx | 6 +++--- src/components/ui/SafeCarousel.tsx | 8 +++++--- src/components/ui/SafeImage.tsx | 8 +++++--- 8 files changed, 45 insertions(+), 36 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 62a6f0a..169cb83 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -5,10 +5,9 @@ import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import typescript from '@typescript-eslint/eslint-plugin' import parser from '@typescript-eslint/parser' -import next from 'eslint-plugin-next' export default [ - { ignores: ['dist', '.next'] }, + { ignores: ['dist', '.next', 'out', 'node_modules'] }, { files: ['**/*.{js,jsx,ts,tsx}'], languageOptions: { @@ -27,7 +26,6 @@ export default [ 'react-hooks': reactHooks, 'react-refresh': reactRefresh, '@typescript-eslint': typescript, - next, }, rules: { ...js.configs.recommended.rules, @@ -35,8 +33,8 @@ export default [ ...react.configs['jsx-runtime'].rules, ...reactHooks.configs.recommended.rules, ...typescript.configs.recommended.rules, - ...next.configs.recommended.rules, 'react/jsx-no-target-blank': 'off', + 'react/prop-types': 'off', // Using TypeScript for prop validation 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true }, diff --git a/src/components/layout/Background.tsx b/src/components/layout/Background.tsx index 1835931..86fcbe6 100644 --- a/src/components/layout/Background.tsx +++ b/src/components/layout/Background.tsx @@ -1,8 +1,12 @@ -const Bg = () => { +import { memo } from "react"; + +const Bg = memo(() => { return (
); -}; +}); + +Bg.displayName = 'Background'; export default Bg; diff --git a/src/components/layout/MyHead.tsx b/src/components/layout/MyHead.tsx index c34e20c..3673959 100644 --- a/src/components/layout/MyHead.tsx +++ b/src/components/layout/MyHead.tsx @@ -38,16 +38,6 @@ function Model({ onLoaded }: { onLoaded: () => void }) { }; }, [scene, animations]); - useEffect(() => { - const animate = () => { - requestAnimationFrame(animate); - if (mixer.current) { - mixer.current.update(0.01); - } - }; - animate(); - }, []); - useEffect(() => { if (head.current) { head.current.rotation.set(0, Math.PI, 0); @@ -55,10 +45,16 @@ function Model({ onLoaded }: { onLoaded: () => void }) { } }, []); - useFrame((state, dt) => { + useFrame((state, delta) => { + // Update animation mixer if active + if (mixer.current) { + mixer.current.update(delta); + } + + // Update head rotation to follow cursor dummy.lookAt(cursor.x, cursor.y, 1); dummy.rotation.y += Math.PI; - easing.dampQ(head.current.quaternion, dummy.quaternion, 0.15, dt); + easing.dampQ(head.current.quaternion, dummy.quaternion, 0.15, delta); }); return ; diff --git a/src/components/layout/Nav.tsx b/src/components/layout/Nav.tsx index bb0cda4..e15aeac 100644 --- a/src/components/layout/Nav.tsx +++ b/src/components/layout/Nav.tsx @@ -27,7 +27,9 @@ const Nav = () => { (entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; - setActiveSection(entry.target.id); + // Only update if the active section has actually changed + const newSection = entry.target.id; + setActiveSection((prev) => prev !== newSection ? newSection : prev); }); }, { threshold: 0.01 }, diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx index 878d872..5d47615 100644 --- a/src/components/sections/Hero.tsx +++ b/src/components/sections/Hero.tsx @@ -1,5 +1,6 @@ import { motion } from "framer-motion"; import dynamic from "next/dynamic"; +import { useCallback } from "react"; import { introductionText } from "../../data"; import Bg from "../layout/Background"; @@ -9,6 +10,16 @@ const HeadRender = dynamic(() => import("../layout/MyHead"), { }); export function Hero() { + const scrollToProjects = useCallback(() => { + const projectsSection = document.getElementById("projects"); + projectsSection?.scrollIntoView({ behavior: "smooth" }); + }, []); + + const scrollToContact = useCallback(() => { + const contactSection = document.getElementById("contact"); + contactSection?.scrollIntoView({ behavior: "smooth" }); + }, []); + return (
@@ -57,19 +68,13 @@ export function Hero() {
-
diff --git a/src/components/sections/Projects.tsx b/src/components/sections/Projects.tsx index 35a289c..395f87b 100644 --- a/src/components/sections/Projects.tsx +++ b/src/components/sections/Projects.tsx @@ -1,4 +1,3 @@ - import { useState, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Projects } from "../../data"; @@ -6,11 +5,13 @@ import { Suspense, lazy } from "react"; const SafeImage = lazy(() => import("../ui/SafeImage")); -type ProjectType = typeof Projects[number]; +type ProjectType = (typeof Projects)[number]; const ProjectsSection = () => { const [loading, setLoading] = useState(true); - const [selectedProject, setSelectedProject] = useState(null); + const [selectedProject, setSelectedProject] = useState( + null, + ); useEffect(() => { const timer = setTimeout(() => setLoading(false), 600); @@ -18,7 +19,10 @@ const ProjectsSection = () => { }, []); const skeletons = Array.from({ length: 3 }); - const openModal = useCallback((proj: ProjectType) => setSelectedProject(proj), []); + const openModal = useCallback( + (proj: ProjectType) => setSelectedProject(proj), + [], + ); const closeModal = useCallback(() => setSelectedProject(null), []); return ( @@ -30,7 +34,10 @@ const ProjectsSection = () => {
{loading ? skeletons.map((_, i) => ( -
+
@@ -66,7 +73,11 @@ const ProjectsSection = () => { {/* Project Icon */}
{proj.icon && ( - }> + + } + > { proj.status === "Live" ? "bg-green-500 bg-opacity-20 text-green-400 border border-green-500 border-opacity-30" : proj.status === "In Development" - ? "bg-yellow-500 bg-opacity-20 text-yellow-400 border border-yellow-500 border-opacity-30" - : "bg-blue-500 bg-opacity-20 text-blue-400 border border-blue-500 border-opacity-30" + ? "bg-yellow-500 bg-opacity-20 text-yellow-400 border border-yellow-500 border-opacity-30" + : "bg-blue-500 bg-opacity-20 text-blue-400 border border-blue-500 border-opacity-30" }`} > {proj.status} @@ -142,7 +153,7 @@ const ProjectsSection = () => { initial={{ scale: 0.95 }} animate={{ scale: 1 }} exit={{ scale: 0.95 }} - onClick={e => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} >