Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"eslint": "8.47.0",
"eslint-config-next": "13.4.13",
"framer-motion": "10.18.0",
"gsap": "^3.12.5",
"html-react-parser": "5.1.1",
"lenis": "^1.1.6",
"lucide-react": "0.316.0",
Expand Down
28 changes: 4 additions & 24 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Poppins } from "next/font/google";
import { Metadata } from "next";
import ReduxProvider from "@/redux/ReduxProvider";
import { Toaster } from "sonner";

import AnimatedCursor from "react-animated-cursor";
import ScrollToTop from "@/components/ScrollToTop/scrolltotop";
import CursorTrail from "@/components/core/cursor/cursorTrail";
import CursorTrailHandler from "@/components/core/cursor/cursorTrailHandler";

import { Providers } from "./providers";
import LenisWrapper from "@/helper/leniswrapper";
Expand Down Expand Up @@ -57,27 +57,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
suppressHydrationWarning
>
<body className={`${poppinsFont.className} bg-white dark:bg-secondary`}>
<AnimatedCursor
innerSize={9}
outerSize={40}
color="2, 2, 2"
outerAlpha={.2}
innerScale={0.7}
outerScale={3}
clickables={[

'a',
'input[type="text"]',
'input[type="email"]',
'input[type="number"]',
'input[type="submit"]',
'input[type="image"]',
'label[for]',
'select',
'textarea',
'button',
'.link'
]} />
{/* Cursor trail handler and trail */}
<Toaster
position="top-right"
duration={3000}
Expand All @@ -87,7 +67,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
theme="light"
/>
<Providers>
<ReduxProvider>{children}</ReduxProvider>
<ReduxProvider><CursorTrail /><CursorTrailHandler />{children}</ReduxProvider>
</Providers>
<ScrollToTop />
</body>
Expand Down
80 changes: 80 additions & 0 deletions src/components/core/cursor/cursorTrail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import { useSelector } from "react-redux";
import { useEffect, useRef } from "react";
import { RootState } from "@/redux/store";

const CursorTrail = () => {
const trailPositions = useSelector((state: RootState) => state.cursor.trailPositions);
const restPosition = useSelector((state: RootState) => state.cursor.position); // Last cursor position
const trailRef = useRef<HTMLDivElement[]>([]); // Refs for trail elements
const animationFrameRef = useRef<number | null>(null);
Comment on lines +8 to +11
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider memoizing selector and adding type safety

The Redux selectors could benefit from memoization to prevent unnecessary re-renders, and the refs could use explicit typing.

Consider these improvements:

+ import { createSelector } from '@reduxjs/toolkit';
+ import { RefObject } from 'react';

+ const selectTrailPositions = createSelector(
+   (state: RootState) => state.cursor.trailPositions,
+   (trailPositions) => trailPositions
+ );

+ const selectRestPosition = createSelector(
+   (state: RootState) => state.cursor.position,
+   (position) => position
+ );

const CursorTrail = () => {
-  const trailPositions = useSelector((state: RootState) => state.cursor.trailPositions);
-  const restPosition = useSelector((state: RootState) => state.cursor.position);
-  const trailRef = useRef<HTMLDivElement[]>([]);
+  const trailPositions = useSelector(selectTrailPositions);
+  const restPosition = useSelector(selectRestPosition);
+  const trailRef = useRef<Array<HTMLDivElement | null>>([]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const trailPositions = useSelector((state: RootState) => state.cursor.trailPositions);
const restPosition = useSelector((state: RootState) => state.cursor.position); // Last cursor position
const trailRef = useRef<HTMLDivElement[]>([]); // Refs for trail elements
const animationFrameRef = useRef<number | null>(null);
import { createSelector } from '@reduxjs/toolkit';
import { RefObject } from 'react';
const selectTrailPositions = createSelector(
(state: RootState) => state.cursor.trailPositions,
(trailPositions) => trailPositions
);
const selectRestPosition = createSelector(
(state: RootState) => state.cursor.position,
(position) => position
);
const CursorTrail = () => {
const trailPositions = useSelector(selectTrailPositions);
const restPosition = useSelector(selectRestPosition);
const trailRef = useRef<Array<HTMLDivElement | null>>([]);
const animationFrameRef = useRef<number | null>(null);


useEffect(() => {
const lerp = (start: number, end: number, factor: number) => {
return start + (end - start) * factor;
};

const animateTrail = () => {
if (trailRef.current) {
trailRef.current.forEach((el, index) => {
if (el) {
// Current element position
const currentX = parseFloat(el.style.left || "0");
const currentY = parseFloat(el.style.top || "0");

// Target position: Interpolate toward trail position or rest position
const targetX = index < trailPositions.length ? trailPositions[index].x : restPosition.x;
const targetY = index < trailPositions.length ? trailPositions[index].y : restPosition.y;

// Smoothly move toward the target position
const newX = lerp(currentX, targetX, 0.2); // Adjust 0.2 for faster catch-up
const newY = lerp(currentY, targetY, 0.2);

// Update element position
el.style.left = `${newX}px`;
el.style.top = `${newY}px`;
}
});
}

animationFrameRef.current = requestAnimationFrame(animateTrail);
};

animationFrameRef.current = requestAnimationFrame(animateTrail);

return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [trailPositions, restPosition]);
Comment on lines +13 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize animation performance and add cleanup

The animation logic could benefit from performance optimizations and better cleanup handling.

  1. Debounce animation frame updates
  2. Add error boundaries
  3. Improve cleanup handling
  useEffect(() => {
+   let isActive = true;
    const lerp = (start: number, end: number, factor: number) => {
      return start + (end - start) * factor;
    };

    const animateTrail = () => {
+     if (!isActive) return;
      if (trailRef.current) {
        trailRef.current.forEach((el, index) => {
          if (el) {
+           try {
              // Current element position
              const currentX = parseFloat(el.style.left || "0");
              const currentY = parseFloat(el.style.top || "0");

              // Target position
              const targetX = index < trailPositions.length ? trailPositions[index].x : restPosition.x;
              const targetY = index < trailPositions.length ? trailPositions[index].y : restPosition.y;

+             // Skip update if change is minimal
+             if (Math.abs(currentX - targetX) < 0.1 && Math.abs(currentY - targetY) < 0.1) {
+               return;
+             }

              // Update position
              const newX = lerp(currentX, targetX, 0.2);
              const newY = lerp(currentY, targetY, 0.2);
              el.style.left = `${newX}px`;
              el.style.top = `${newY}px`;
+           } catch (error) {
+             console.error('Error updating trail position:', error);
+           }
          }
        });
      }

      animationFrameRef.current = requestAnimationFrame(animateTrail);
    };

    animationFrameRef.current = requestAnimationFrame(animateTrail);

    return () => {
+     isActive = false;
      if (animationFrameRef.current) {
        cancelAnimationFrame(animationFrameRef.current);
+       animationFrameRef.current = null;
      }
    };
  }, [trailPositions, restPosition]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const lerp = (start: number, end: number, factor: number) => {
return start + (end - start) * factor;
};
const animateTrail = () => {
if (trailRef.current) {
trailRef.current.forEach((el, index) => {
if (el) {
// Current element position
const currentX = parseFloat(el.style.left || "0");
const currentY = parseFloat(el.style.top || "0");
// Target position: Interpolate toward trail position or rest position
const targetX = index < trailPositions.length ? trailPositions[index].x : restPosition.x;
const targetY = index < trailPositions.length ? trailPositions[index].y : restPosition.y;
// Smoothly move toward the target position
const newX = lerp(currentX, targetX, 0.2); // Adjust 0.2 for faster catch-up
const newY = lerp(currentY, targetY, 0.2);
// Update element position
el.style.left = `${newX}px`;
el.style.top = `${newY}px`;
}
});
}
animationFrameRef.current = requestAnimationFrame(animateTrail);
};
animationFrameRef.current = requestAnimationFrame(animateTrail);
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [trailPositions, restPosition]);
useEffect(() => {
let isActive = true;
const lerp = (start: number, end: number, factor: number) => {
return start + (end - start) * factor;
};
const animateTrail = () => {
if (!isActive) return;
if (trailRef.current) {
trailRef.current.forEach((el, index) => {
if (el) {
try {
// Current element position
const currentX = parseFloat(el.style.left || "0");
const currentY = parseFloat(el.style.top || "0");
// Target position
const targetX = index < trailPositions.length ? trailPositions[index].x : restPosition.x;
const targetY = index < trailPositions.length ? trailPositions[index].y : restPosition.y;
// Skip update if change is minimal
if (Math.abs(currentX - targetX) < 0.1 && Math.abs(currentY - targetY) < 0.1) {
return;
}
// Update position
const newX = lerp(currentX, targetX, 0.2);
const newY = lerp(currentY, targetY, 0.2);
el.style.left = `${newX}px`;
el.style.top = `${newY}px`;
} catch (error) {
console.error('Error updating trail position:', error);
}
}
});
}
animationFrameRef.current = requestAnimationFrame(animateTrail);
};
animationFrameRef.current = requestAnimationFrame(animateTrail);
return () => {
isActive = false;
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
animationFrameRef.current = null;
}
};
}, [trailPositions, restPosition]);


return (
<>
{Array(10)
.fill(0)
.map((_, index) => (
<div
key={index}
ref={(el) => {
if (el) trailRef.current[index] = el;
}}
style={{
position: "fixed",
left: `0px`, // Initial position
top: `0px`,
width: `${15 - index}px`, // Increase size here
height: `${15 - index}px`, // Increase size here
backgroundColor: `rgba(255, 0, 0, ${0.5 - index * 0.05})`, // Fade with index
borderRadius: "50%",
pointerEvents: "none",
transform: "translate(-50%, -50%)",
}}
/>
))}
</>
);
Comment on lines +53 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance accessibility and performance

The trail implementation could benefit from accessibility improvements and performance optimizations.

  1. Add ARIA attributes
  2. Use CSS transforms for better performance
  3. Consider reducing DOM elements for better performance
  4. Add media query support for reduced motion
+ const TRAIL_LENGTH = 10;
+ const BASE_SIZE = 15;
+ const BASE_OPACITY = 0.5;

return (
+   <div aria-hidden="true" className="cursor-trail-container">
-   <>
-     {Array(10)
+     {Array(TRAIL_LENGTH)
        .fill(0)
        .map((_, index) => (
          <div
            key={index}
            ref={(el) => {
              if (el) trailRef.current[index] = el;
            }}
            style={{
              position: "fixed",
              left: `0px`,
              top: `0px`,
-             width: `${15 - index}px`,
-             height: `${15 - index}px`,
-             backgroundColor: `rgba(255, 0, 0, ${0.5 - index * 0.05})`,
+             width: `${BASE_SIZE - index}px`,
+             height: `${BASE_SIZE - index}px`,
+             backgroundColor: `rgba(255, 0, 0, ${BASE_OPACITY - index * 0.05})`,
              borderRadius: "50%",
              pointerEvents: "none",
-             transform: "translate(-50%, -50%)",
+             transform: "translate(-50%, -50%) translateZ(0)",
+             willChange: "transform",
            }}
          />
        ))}
-   </>
+   </div>

Also, add this CSS to your global styles:

@media (prefers-reduced-motion: reduce) {
  .cursor-trail-container {
    display: none;
  }
}

};

export default CursorTrail;
26 changes: 26 additions & 0 deletions src/components/core/cursor/cursorTrailHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { useEffect } from "react";
import { useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { updateCursorPosition } from "@/redux/reducers/cursorReducer";

const CursorTrailHandler = () => {
const dispatch = useDispatch<AppDispatch>();

useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
dispatch(updateCursorPosition({ x: event.clientX, y: event.clientY }));
};

window.addEventListener("mousemove", handleMouseMove);

return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [dispatch]);
Comment on lines +11 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize mouse move event handling

The current implementation dispatches an action on every mouse move, which could impact performance.

Consider throttling the mouse move handler:

+import { throttle } from 'lodash';
+
 const CursorTrailHandler = () => {
   const dispatch = useDispatch<AppDispatch>();
 
   useEffect(() => {
-    const handleMouseMove = (event: MouseEvent) => {
+    const handleMouseMove = throttle((event: MouseEvent) => {
       dispatch(updateCursorPosition({ x: event.clientX, y: event.clientY }));
-    };
+    }, 16); // ~60fps
 
     window.addEventListener("mousemove", handleMouseMove);
 
     return () => {
+      handleMouseMove.cancel();
       window.removeEventListener("mousemove", handleMouseMove);
     };
   }, [dispatch]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const handleMouseMove = (event: MouseEvent) => {
dispatch(updateCursorPosition({ x: event.clientX, y: event.clientY }));
};
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [dispatch]);
import { throttle } from 'lodash';
useEffect(() => {
const handleMouseMove = throttle((event: MouseEvent) => {
dispatch(updateCursorPosition({ x: event.clientX, y: event.clientY }));
}, 16); // ~60fps
window.addEventListener("mousemove", handleMouseMove);
return () => {
handleMouseMove.cancel();
window.removeEventListener("mousemove", handleMouseMove);
};
}, [dispatch]);


return null;
};

export default CursorTrailHandler;
41 changes: 41 additions & 0 deletions src/redux/reducers/cursorReducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import PageLoader from "next/dist/client/page-loader";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused import

The PageLoader import is not used in this file.

-import PageLoader from "next/dist/client/page-loader";


interface Position {
x: number;
y: number;
}

interface CursorState {
position: Position;
trailPositions: Position[];
trailLength: number;
}

const initialState: CursorState = {
position: {x:0, y:0},
trailPositions: [],
trailLength:20,
}

const cursorSlice = createSlice({
name: "cursor",
initialState,
reducers: {
updateCursorPosition(state, action: PayloadAction<Position>) {
state.position = action.payload;
state.trailPositions.push(action.payload);
if(state.trailPositions.length > state.trailLength)
{
state.trailPositions.shift();
}
},
Comment on lines +25 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add performance optimization for trail updates

The current implementation might cause unnecessary re-renders by mutating the array on every mouse move.

Consider these optimizations:

  1. Throttle updates using a debounce/throttle mechanism
  2. Use array size check before push
 updateCursorPosition(state, action: PayloadAction<Position>) {
     state.position = action.payload;
-    state.trailPositions.push(action.payload);
-    if(state.trailPositions.length > state.trailLength)
-    {
+    if (state.trailPositions.length >= state.trailLength) {
         state.trailPositions.shift();
     }
+    state.trailPositions.push(action.payload);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
updateCursorPosition(state, action: PayloadAction<Position>) {
state.position = action.payload;
state.trailPositions.push(action.payload);
if(state.trailPositions.length > state.trailLength)
{
state.trailPositions.shift();
}
},
updateCursorPosition(state, action: PayloadAction<Position>) {
state.position = action.payload;
if (state.trailPositions.length >= state.trailLength) {
state.trailPositions.shift();
}
state.trailPositions.push(action.payload);
},

setTrailLength(state, action:PayloadAction<number>)
{
state.trailLength = action.payload;
},
Comment on lines +33 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation for trail length

The setTrailLength reducer should validate the input to prevent negative or zero values.

 setTrailLength(state, action:PayloadAction<number>) {
+    if (action.payload <= 0) {
+        throw new Error('Trail length must be greater than 0');
+    }
     state.trailLength = action.payload;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTrailLength(state, action:PayloadAction<number>)
{
state.trailLength = action.payload;
},
setTrailLength(state, action:PayloadAction<number>)
{
if (action.payload <= 0) {
throw new Error('Trail length must be greater than 0');
}
state.trailLength = action.payload;
},

},
});

export const { updateCursorPosition, setTrailLength } = cursorSlice.actions;
export default cursorSlice.reducer;
5 changes: 5 additions & 0 deletions src/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import { configureStore } from "@reduxjs/toolkit";
import postsReducer from "@/redux/reducers/postsReducer";
import authReducer from "@/redux/reducers/authReducer";
import bookmarkReducer from "@/redux/reducers/bookmarkReducer";
import cursorReducer from "@/redux/reducers/cursorReducer";

export const store = configureStore({
reducer: {
posts: postsReducer,
auth: authReducer,
bookmarks: bookmarkReducer,
cursor: cursorReducer,
},
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Loading
Loading