Skip to content

feat: build 3D Creator portfolio landing page - #8

Open
SayanthRock wants to merge 1 commit into
mainfrom
feat/3d-creator-portfolio-221361022642684091
Open

feat: build 3D Creator portfolio landing page#8
SayanthRock wants to merge 1 commit into
mainfrom
feat/3d-creator-portfolio-221361022642684091

Conversation

@SayanthRock

@SayanthRock SayanthRock commented Jul 31, 2026

Copy link
Copy Markdown
Owner

User description

  • Implemented Hero, Marquee, About, Services, and Projects sections with required design and content.
  • Applied responsive fluid typography with clamp() and dark theme (#0C0C0C).
  • Built reusable framer-motion components: FadeIn, Magnet, AnimatedText.
  • Developed scroll-driven Marquee and sticky-stacking ProjectCards.
  • Cleaned up unused dependency warning from lucide-react and fixed Magnet transition issue.

PR created automatically by Jules for task 221361022642684091 started by @SayanthRock


CodeAnt-AI Description

Refine Jack’s 3D creator portfolio with responsive visuals and smoother interactions

What Changed

  • Added Jack’s portrait to the hero area with mouse-responsive movement and animated entrance.
  • Updated project cards with responsive image layouts, sticky stacking, scroll-based scaling, and mobile-friendly project buttons.
  • Marquee rows now move in opposite directions while scrolling, with smoother frame-based updates.
  • About and services content now uses staged entrance animations, while the about text reveals progressively by character.
  • Contact and live-project buttons support custom labels, with clearer hover and active feedback.
  • Removed the GitHub navigation icon and unused production dependencies.

Impact

✅ Clearer 3D creator introduction
✅ Smoother scroll-driven portfolio browsing
✅ Easier project actions on mobile

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

Summary by CodeRabbit

  • New Features

    • Added customizable labels for contact and live project buttons.
    • Improved responsive layouts across the hero, about, projects, and services sections.
    • Enhanced animated text, magnetic interactions, marquee scrolling, and fade-in effects.
  • Bug Fixes

    • Improved animation positioning and responsiveness.
    • Updated project and service content rendering for more consistent layouts.
    • Removed an unused GitHub navigation link.
  • Style

    • Refined button hover states, spacing, typography, borders, and image presentation throughout the site.

Co-authored-by: SayanthRock <202829406+SayanthRock@users.noreply.github.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sayanth-rock-github-io Ready Ready Preview Jul 31, 2026 10:42pm

@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI review requested due to automatic review settings July 31, 2026 22:41
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@devloai

devloai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Unable to trigger custom agent "Code Reviewer". You have run out of credits 😔
Please upgrade your plan or buy additional credits from the subscription page.

@performance-testing-bot

Copy link
Copy Markdown

Unable to locate .performanceTestingBot config file

@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 4f7a65f Jul 31, 2026 · 22:42 22:44

@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request updates shared animated components and button APIs, revises portfolio section layouts and content fields, replaces marquee state updates with ref-based animation, and redesigns project cards with responsive live-project actions.

Changes

Portfolio UI changes

Layer / File(s) Summary
Shared component APIs and animations
package.json, src/components/*
Shared components now use named function exports, narrower props, typed animation helpers, and Framer Motion values. Runtime dependency classification was updated.
Hero, about, and services layouts
src/sections/AboutSection.tsx, src/sections/HeroSection.tsx, src/sections/ServicesSection.tsx
Section wrappers, responsive spacing, navigation content, animation offsets, and service metadata fields were updated.
Marquee scroll rendering
src/sections/MarqueeSection.tsx
Marquee rows now use refs, requestAnimationFrame, direct transforms, and animation-frame cleanup.
Project data and card rendering
src/sections/ProjectsSection.tsx
Project fields now use name. Cards now include responsive layouts, scroll scaling, lazy-loaded images, and desktop or mobile live-project buttons.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant MarqueeSection
  participant RowRefs
  Browser->>MarqueeSection: scroll event
  MarqueeSection->>MarqueeSection: schedule updateScroll with requestAnimationFrame
  MarqueeSection->>RowRefs: apply opposing transforms
  MarqueeSection->>Browser: cancel pending frame during cleanup
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: implementing a 3D Creator portfolio landing page.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/3d-creator-portfolio-221361022642684091

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Jul 31, 2026

@ai-coding-guardrails ai-coding-guardrails Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I've got 3 comments for you to consider

List of skipped files due to configuration

Risk: 🟢 Low

Risk analysis

The highest scoring dimensions are operational_risk and blast_radius. operational_risk is elevated due to critical runtime dependencies (framer-motion, clsx, tailwind-merge) being misplaced in devDependencies, which will cause runtime failures in production builds. blast_radius is moderate because the changes span 11 files and affect multiple frontend components and sections of the portfolio page, impacting user experience across the landing page. reversibility is low since the dependency misplacement and component logic changes would require code modifications to revert, but do not involve irreversible data or infrastructure changes.

Did you know we can integrate this feedback directly into 50+ IDEs? Get setup in just one command

Comment thread package.json
Comment on lines +23 to +24
"clsx": "^2.1.1",
"framer-motion": "^12.43.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

framer-motion and clsx are runtime dependencies moved to devDependencies, causing missing modules in production builds

Both framer-motion and clsx are used at runtime in the application (e.g., in FadeIn.tsx, AnimatedText.tsx, and other components), but they have been moved to devDependencies. This means they will not be included in production builds/deployments that install only dependencies, resulting in runtime errors. They should remain in (or be moved back to) dependencies.

Similarly, tailwind-merge appears to be used at runtime and is also in devDependencies.

Why did I show this?

Category: bug
Comment Quality: high

Based on general best practices

Comment on lines +54 to +57
const onScroll = () => {
if (Math.abs(window.scrollY - lastScrollY) > 0) {
animationFrameId = requestAnimationFrame(updateScroll);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

animationFrameId may be uninitialized when cancelAnimationFrame is called on cleanup if scroll never fired

If the scroll event never fires before the component unmounts, animationFrameId will be undefined (its declared type is number but it's never assigned). Calling cancelAnimationFrame(undefined) is harmless in browsers, but more critically, if scroll fires rapidly, each onScroll call schedules a new rAF without cancelling the previous one, potentially queuing multiple updateScroll frames per scroll event. The previous animationFrameId should be cancelled before scheduling a new one inside onScroll.

Why did I show this?

Category: bug
Comment Quality: high

Based on general best practices

const scale = useTransform(scaleProgress, [0, 1], [1, index === totalCards - 1 ? 1 : targetScale]);

// Transform scale based on scroll position
const scale = useTransform(scrollYProgress, [0, 1], [1, targetScale]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Last card scales down unnecessarily because targetScale is always less than 1

The targetScale formula 1 - (totalCards - 1 - index) * 0.03 evaluates to 1.0 only for the last card (index === totalCards - 1). However, the scale transform [1, targetScale] still animates from 1 to 1 for the last card, which is fine — but for all other cards it will scale them down as they scroll, regardless of whether a subsequent card has actually overlapped them. The previous implementation explicitly guarded the last card with index === totalCards - 1 ? 1 : targetScale, which was intentional. More critically, the targetScale for the first card (index=0, totalCards=3) becomes 1 - 2*0.03 = 0.94, meaning it shrinks the most even though it's at the bottom of the stack — this is inverted from typical stacking card behaviour where cards higher in the stack (lower index) should shrink more as later cards stack on top. The scale values are effectively backwards: the first card should scale down the most, but targetScale gives it the smallest value, which is correct, yet the useScroll offset ['start start', 'end start'] means the card scales as it scrolls out, not as the next card arrives. This can cause all cards to be partially scaled before any stacking occurs visually.

Why did I show this?

Category: bug
Comment Quality: high

Based on general best practices

@ai-coding-guardrails ai-coding-guardrails Bot added the zenable/risk:low Zenable assessed this PR as LOW risk. label Jul 31, 2026
className="text-[#D7E2EA] hover:opacity-70 transition-opacity duration-200 ml-4 md:ml-8"
aria-label="GitHub Profile"
key={item}
href={`#${item.toLowerCase()}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The generated #price and #contact links have no matching anchors in the mounted sections, so clicking those navbar items does not navigate anywhere. Use existing section IDs such as #services or add the corresponding anchors and contact destination. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ Price navigation has no destination.
- ⚠️ Contact navigation has no destination.
- ⚠️ Users cannot reach advertised sections from the navbar.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/sections/HeroSection.tsx
**Line:** 13:13
**Comment:**
	*Api Mismatch: The generated `#price` and `#contact` links have no matching anchors in the mounted sections, so clicking those navbar items does not navigate anywhere. Use existing section IDs such as `#services` or add the corresponding anchors and contact destination.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +107 to +108
<LiveProjectButton />
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Both newly rendered “Live Project” controls are plain buttons with no click handler, form action, or link target, so users cannot open any project despite the control's label. Provide a project URL and render an anchor or attach the intended navigation handler. [incomplete implementation]

Severity Level: Major ⚠️
- ❌ Desktop project controls cannot open projects.
- ❌ Mobile project controls cannot open projects.
- ⚠️ Portfolio visitors cannot inspect advertised live work.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/sections/ProjectsSection.tsx
**Line:** 107:108
**Comment:**
	*Incomplete Implementation: Both newly rendered “Live Project” controls are plain buttons with no click handler, form action, or link target, so users cannot open any project despite the control's label. Provide a project URL and render an anchor or attach the intended navigation handler.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +10 to +24
<button
className={cn(
"rounded-full outline outline-2 outline-white outline-offset-[-3px]",
"px-8 py-3 sm:px-10 sm:py-3.5 md:px-12 md:py-4",
"text-xs sm:text-sm md:text-base text-white font-medium uppercase tracking-widest",
"transition-transform hover:scale-105 active:scale-95",
className
)}
style={{
background: "linear-gradient(123deg, #18011F 7%, #B600A8 37%, #7621B0 72%, #BE4C00 100%)",
boxShadow: "0px 4px 4px rgba(181, 1, 167, 0.25), inset 4px 4px 12px #7721B1"
}}
>
{label}
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The rendered CTA has no onClick, link target, form action, or navigation behavior, so both visible “Contact Me” controls respond to clicks without taking the user to a contact destination. Add an explicit contact action or render this as a link to the contact section. [api mismatch]

Severity Level: Major ⚠️
- ❌ Hero contact CTA performs no action.
- ❌ About contact CTA performs no action.
- ⚠️ Navbar contact anchor has no matching section.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/components/ContactButton.tsx
**Line:** 10:24
**Comment:**
	*Api Mismatch: The rendered CTA has no `onClick`, link target, form action, or navigation behavior, so both visible “Contact Me” controls respond to clicks without taking the user to a contact destination. Add an explicit contact action or render this as a link to the contact section.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +9 to +21
return (
<button
className={cn(
"rounded-full border-2 border-[#D7E2EA]",
"px-8 py-3 sm:px-10 sm:py-3.5",
"text-sm sm:text-base text-[#D7E2EA] font-medium uppercase tracking-widest",
"hover:bg-[#D7E2EA]/10 transition-colors duration-200",
className
)}
>
{label}
</button>
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: The “Live Project” control has no project URL, onClick handler, or navigation behavior, so every project card displays a button that cannot open the corresponding project. Accept a destination or callback and wire it to the project data, preferably rendering an anchor for external URLs. [api mismatch]

Severity Level: Major ⚠️
- ❌ Desktop project buttons cannot open projects.
- ❌ Mobile project buttons cannot open projects.
- ⚠️ All three project cards lack destinations.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/components/LiveProjectButton.tsx
**Line:** 9:21
**Comment:**
	*Api Mismatch: The “Live Project” control has no project URL, `onClick` handler, or navigation behavior, so every project card displays a button that cannot open the corresponding project. Accept a destination or callback and wire it to the project data, preferably rendering an anchor for external URLs.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (1)
src/sections/MarqueeSection.tsx (1)

76-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use distinct or empty alt text instead of repeating the same generic label.

Every image in both rows shares the identical alt="Marquee item" (lines 79 and 92). For a marquee rendering many images, a screen reader announces this same non-descriptive phrase for each one, which gives no useful information and is repetitive noise for assistive-technology users. If the marquee is purely decorative, use alt="" so screen readers skip it; if the images convey meaningful content, give each one a distinct, descriptive alt.

🖼️ Proposed fix (decorative marquee)
             <img
               key={`row1-${i}`}
               src={url}
-              alt="Marquee item"
+              alt=""
               loading="lazy"
               className="w-[420px] h-[270px] rounded-2xl object-cover flex-shrink-0"
             />

Also applies to: 88-96

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sections/MarqueeSection.tsx` around lines 76 - 83, Update the images
rendered in both marquee rows within MarqueeSection so their alt text is not the
repeated generic “Marquee item”; use empty alt text if the marquee is
decorative, or derive distinct descriptive text from each image’s available data
if the images convey meaning.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Around line 17-27: Move clsx, framer-motion, and tailwind-merge from
devDependencies to dependencies in package.json, leaving the other dependency
classifications unchanged so production installs include all runtime-imported
packages.

In `@src/components/AnimatedText.tsx`:
- Around line 9-31: Update AnimatedText and its character wrapper so the
duplicated visual character spans are marked aria-hidden="true", while the
parent paragraph exposes the complete sentence once through aria-label={text}.
Preserve the existing animation and layout behavior.

In `@src/components/LiveProjectButton.tsx`:
- Around line 8-22: Update src/components/LiveProjectButton.tsx:8-22 and both
call sites in src/sections/ProjectsSection.tsx:106-108 and 129-132 to pass and
use each project’s live URL, making the button navigate to that target. Extend
the project data with a liveUrl or href field and preserve the existing label
and styling for both project entries.

In `@src/components/Magnet.tsx`:
- Line 18: Remove the unused isActive state and its setter updates from the
Magnet component’s mousemove handling, and remove the corresponding useState
import. Preserve the existing stateless x/y Motion value behavior.
- Around line 27-52: Update the useEffect mouse-tracking logic in Magnet to
listen for the window’s pointer-leave event and reset x and y to 0, also
clearing isActive when appropriate. Register the new listener alongside
mousemove and remove it in the effect cleanup, preserving the existing movement
behavior while the pointer remains in the viewport.

In `@src/sections/HeroSection.tsx`:
- Around line 30-43: Move the absolute positioning and centering classes from
the FadeIn element to a wrapper around the faded/magnetized content, keeping
left-1/2 and -translate-x-1/2 on that wrapper. Leave FadeIn to animate the inner
portrait without conflicting layout transforms, while preserving the existing
responsive positioning and sizing behavior.

In `@src/sections/MarqueeSection.tsx`:
- Around line 35-68: Replace the ineffective scroll-position check in the
MarqueeSection useEffect with an rAF scheduling guard, such as a ticking flag or
null check, so onScroll queues at most one updateScroll callback until the
pending frame runs. Clear the guard inside the scheduled callback, preserve the
initial updateScroll call, and ensure cleanup cancels any pending frame and
removes the listener.

---

Nitpick comments:
In `@src/sections/MarqueeSection.tsx`:
- Around line 76-83: Update the images rendered in both marquee rows within
MarqueeSection so their alt text is not the repeated generic “Marquee item”; use
empty alt text if the marquee is decorative, or derive distinct descriptive text
from each image’s available data if the images convey meaning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e33ac06d-23d9-470e-beeb-880e6a57fb2d

📥 Commits

Reviewing files that changed from the base of the PR and between 506efd0 and 4f7a65f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • package.json
  • src/components/AnimatedText.tsx
  • src/components/ContactButton.tsx
  • src/components/FadeIn.tsx
  • src/components/LiveProjectButton.tsx
  • src/components/Magnet.tsx
  • src/sections/AboutSection.tsx
  • src/sections/HeroSection.tsx
  • src/sections/MarqueeSection.tsx
  • src/sections/ProjectsSection.tsx
  • src/sections/ServicesSection.tsx

Comment thread package.json
Comment on lines 17 to +27
"devDependencies": {
"@types/node": "^24.13.2",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"autoprefixer": "^10.5.4",
"clsx": "^2.1.1",
"framer-motion": "^12.43.0",
"oxlint": "^1.71.0",
"postcss": "^8.5.24",
"postcss": "^8.5.25",
"tailwind-merge": "^3.6.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm clsx/framer-motion/tailwind-merge are imported by shipped (non-dev/test) source files.
rg -n "from 'framer-motion'|from \"framer-motion\"|from 'clsx'|from \"clsx\"|from 'tailwind-merge'|from \"tailwind-merge\"" src --type=ts --type=tsx

Repository: SayanthRock/SayanthRock.github.io

Length of output: 203


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json relevant sections =="
python3 - <<'PY'
import json
with open('package.json') as f:
    pkg=json.load(f)
for section in ['dependencies','devDependencies','scripts']:
    if section in pkg:
        print(f'### {section}')
        print(json.dumps({k:v for k,v in pkg[section].items() if k in ['clsx','framer-motion','tailwind-merge','react','react-dom','vite','`@vitejs/plugin-react`'] or k.startswith('-')}, indent=2))
PY

echo
echo "== direct imports of clsx/framer-motion/tailwind-merge =="
rg -n "from ['\"](framer-motion|clsx|tailwind-merge)['\"]|require\\(['\"](framer-motion|clsx|tailwind-merge)['\"]\\)" . -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' || true

echo
echo "== imports from src/utils/cn.ts and usage around cn =="
rg -n "from ['\"]`@/utils/cn`|from ['\"].*utils/cn|['\"](`@/utils/cn`|src/utils/cn)['\"].*import|cn\\(" . -g '*.ts' -g '*.tsx' || true

Repository: SayanthRock/SayanthRock.github.io

Length of output: 1416


Move runtime-imported packages back to dependencies.

clsx, framer-motion, and tailwind-merge are currently installed through devDependencies, but application code imports them directly for AnimatedText, FadeIn, Magnet, ProjectsSection, and the cn helper used by production components. Move clsx, framer-motion, and tailwind-merge back to dependencies so npm install --production does not fail during production installs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 17 - 27, Move clsx, framer-motion, and
tailwind-merge from devDependencies to dependencies in package.json, leaving the
other dependency classifications unchanged so production installs include all
runtime-imported packages.

Comment on lines +9 to +31
export function AnimatedText({ text, className = "" }: AnimatedTextProps) {
const element = useRef<HTMLParagraphElement>(null);
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start 0.8', 'end 0.2']
target: element,
offset: ['start 0.8', 'end 0.2'],
});

const words = text.split(" ");

return (
<p ref={containerRef} className={cn("flex flex-wrap justify-center", className)}>
{words.map((word, i) => (
<span key={i} className="mr-[0.25em] mb-[0.1em] relative inline-block">
{word.split("").map((char, j) => {
const start = (i * 10 + j) / (words.length * 10);
const end = start + (1 / (words.length * 10));

return (
<Character
key={j}
char={char}
progress={scrollYProgress}
start={start}
end={end}
/>
);
})}
</span>
))}
<p ref={element} className={className} style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center' }}>
{words.map((word, i) => {
const start = i / words.length;
const end = start + (1 / words.length);
return (
<Word key={i} progress={scrollYProgress} range={[start, end]}>
{word}
</Word>
);
})}
</p>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hide duplicated characters from assistive technology.

Each character renders twice: once in a opacity: 0 layout span, once in the animated motion.span. opacity: 0 does not remove content from the accessibility tree, so screen readers announce every character twice. Add aria-hidden="true" to the character wrapper and expose the full sentence once via aria-label on the parent <p>.

♿ Proposed fix
     <p ref={element} className={className} style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'center' }}>
+      <span aria-hidden="true" style={{ display: 'contents' }}>
       {words.map((word, i) => {
         const start = i / words.length;
         const end = start + (1 / words.length);
         return (
           <Word key={i} progress={scrollYProgress} range={[start, end]}>
             {word}
           </Word>
         );
       })}
+      </span>
     </p>

Then add aria-label={text} on the <p> so the full sentence is announced once.

Also applies to: 65-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AnimatedText.tsx` around lines 9 - 31, Update AnimatedText and
its character wrapper so the duplicated visual character spans are marked
aria-hidden="true", while the parent paragraph exposes the complete sentence
once through aria-label={text}. Preserve the existing animation and layout
behavior.

Comment on lines +8 to +22
export function LiveProjectButton({ className, label = "Live Project" }: LiveProjectButtonProps) {
return (
<button
className={cn(
"rounded-full border-2 border-[#D7E2EA]",
"px-8 py-3 sm:px-10 sm:py-3.5",
"text-sm sm:text-base text-[#D7E2EA] font-medium uppercase tracking-widest",
"hover:bg-[#D7E2EA]/10 transition-colors duration-200",
className
)}
>
{label}
</button>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether project records carry a URL/link field that isn't wired to LiveProjectButton.
rg -n "url|href|link|window\.open" src/sections/ProjectsSection.tsx src/components/LiveProjectButton.tsx
rg -n "^\s*(num|label|name|images|url):" -A1 src/sections/ProjectsSection.tsx | head -60

Repository: SayanthRock/SayanthRock.github.io

Length of output: 3418


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline src/components/LiveProjectButton.tsx src/sections/ProjectsSection.tsx || true

echo "== LiveProjectButton =="
cat -n src/components/LiveProjectButton.tsx

echo "== ProjectsSection relevant parts =="
sed -n '1,160p' src/sections/ProjectsSection.tsx | cat -n

echo "== LiveProjectButton usages =="
rg -n "LiveProjectButton|projectsData|projects|Project" src/sections/ProjectsSection.tsx src/components/LiveProjectButton.tsx

Repository: SayanthRock/SayanthRock.github.io

Length of output: 10163


Wire a live URL into LiveProjectButton.

projects only defines num, label, name, and images, and neither call site passes a handler or URL. Add a project liveUrl/href field and pass it into LiveProjectButton, or change the component to accept an onClick target; otherwise the "Live Project" buttons have no action.

📍 Affects 2 files
  • src/components/LiveProjectButton.tsx#L8-L22 (this comment)
  • src/sections/ProjectsSection.tsx#L106-L108
  • src/sections/ProjectsSection.tsx#L129-L132
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/LiveProjectButton.tsx` around lines 8 - 22, Update
src/components/LiveProjectButton.tsx:8-22 and both call sites in
src/sections/ProjectsSection.tsx:106-108 and 129-132 to pass and use each
project’s live URL, making the button navigate to that target. Extend the
project data with a liveUrl or href field and preserve the existing label and
styling for both project entries.

Comment thread src/components/Magnet.tsx
className = "",
}: MagnetProps) {
const ref = useRef<HTMLDivElement>(null);
const [, setIsActive] = useState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate file =="
fd -a 'Magnet\.tsx$' . | sed 's#^\./##'

echo "== Magnet.tsx outline/contents =="
path=$(fd 'Magnet\.tsx$' . | head -n 1)
if [ -n "${path:-}" ]; then
  wc -l "$path"
  sed -n '1,120p' "$path" | cat -n
fi

echo "== Hook imports in Magnet.tsx =="
fd 'Magnet\.tsx$' . -x sh -c '
  file="$1"
  echo "--- $file ---"
  rg -n "useState|useEffect|ref|onMouseMove|mousemove|isActive|setIsActive|motion" "$file"
' sh {}

Repository: SayanthRock/SayanthRock.github.io

Length of output: 3188


Remove the unused isActive state from Magnet.

isActive is only written via setIsActive(true/false) in the mousemove handler, and its value is never read. Keep x/y stateless on the Motion values, and remove the useState import as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Magnet.tsx` at line 18, Remove the unused isActive state and
its setter updates from the Magnet component’s mousemove handling, and remove
the corresponding useState import. Preserve the existing stateless x/y Motion
value behavior.

Comment thread src/components/Magnet.tsx
Comment on lines 27 to +52
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!magnetRef.current) return;
if (!ref.current) return;

const { clientX, clientY } = e;
const { left, top, width, height } = magnetRef.current.getBoundingClientRect();
const { left, top, width, height } = ref.current.getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;

const distanceX = clientX - centerX;
const distanceY = clientY - centerY;
const distanceX = e.clientX - centerX;
const distanceY = e.clientY - centerY;
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);

// Check if mouse is within padding area
if (distance < width / 2 + padding || distance < height / 2 + padding) {
if (distance < padding) {
setIsActive(true);
setPosition({
x: distanceX / strength,
y: distanceY / strength
});
x.set(distanceX / strength);
y.set(distanceY / strength);
} else {
setIsActive(false);
setPosition({ x: 0, y: 0 });
x.set(0);
y.set(0);
}
};

const handleMouseLeave = () => {
setIsActive(false);
setPosition({ x: 0, y: 0 });
};

window.addEventListener('mousemove', handleMouseMove);
document.documentElement.addEventListener('mouseleave', handleMouseLeave);

return () => {
window.removeEventListener('mousemove', handleMouseMove);
document.documentElement.removeEventListener('mouseleave', handleMouseLeave);
};
}, [padding, strength]);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [padding, strength, x, y]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset position when the pointer leaves the window.

The effect only listens for mousemove. If the pointer leaves the browser window while the element is displaced, it stays offset until a subsequent mousemove inside the window recalculates the distance. Add a listener to reset x/y to 0 when the pointer leaves the viewport.

🔧 Proposed fix
+    const handleMouseLeave = () => {
+      x.set(0);
+      y.set(0);
+    };
+
     window.addEventListener('mousemove', handleMouseMove);
-    return () => window.removeEventListener('mousemove', handleMouseMove);
+    document.addEventListener('mouseleave', handleMouseLeave);
+    return () => {
+      window.removeEventListener('mousemove', handleMouseMove);
+      document.removeEventListener('mouseleave', handleMouseLeave);
+    };
📝 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 = (e: MouseEvent) => {
if (!magnetRef.current) return;
if (!ref.current) return;
const { clientX, clientY } = e;
const { left, top, width, height } = magnetRef.current.getBoundingClientRect();
const { left, top, width, height } = ref.current.getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;
const distanceX = clientX - centerX;
const distanceY = clientY - centerY;
const distanceX = e.clientX - centerX;
const distanceY = e.clientY - centerY;
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
// Check if mouse is within padding area
if (distance < width / 2 + padding || distance < height / 2 + padding) {
if (distance < padding) {
setIsActive(true);
setPosition({
x: distanceX / strength,
y: distanceY / strength
});
x.set(distanceX / strength);
y.set(distanceY / strength);
} else {
setIsActive(false);
setPosition({ x: 0, y: 0 });
x.set(0);
y.set(0);
}
};
const handleMouseLeave = () => {
setIsActive(false);
setPosition({ x: 0, y: 0 });
};
window.addEventListener('mousemove', handleMouseMove);
document.documentElement.addEventListener('mouseleave', handleMouseLeave);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
document.documentElement.removeEventListener('mouseleave', handleMouseLeave);
};
}, [padding, strength]);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, [padding, strength, x, y]);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!ref.current) return;
const { left, top, width, height } = ref.current.getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;
const distanceX = e.clientX - centerX;
const distanceY = e.clientY - centerY;
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
if (distance < padding) {
setIsActive(true);
x.set(distanceX / strength);
y.set(distanceY / strength);
} else {
setIsActive(false);
x.set(0);
y.set(0);
}
};
const handleMouseLeave = () => {
x.set(0);
y.set(0);
};
window.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseleave', handleMouseLeave);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseleave', handleMouseLeave);
};
}, [padding, strength, x, y]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Magnet.tsx` around lines 27 - 52, Update the useEffect
mouse-tracking logic in Magnet to listen for the window’s pointer-leave event
and reset x and y to 0, also clearing isActive when appropriate. Register the
new listener alongside mousemove and remove it in the effect cleanup, preserving
the existing movement behavior while the pointer remains in the viewport.

Comment on lines +30 to +43
{/* Hero Portrait */}
<FadeIn
delay={0.6}
y={30}
className="absolute left-1/2 -translate-x-1/2 top-1/2 -translate-y-1/2 sm:top-auto sm:translate-y-0 sm:bottom-0 z-10 w-[280px] sm:w-[360px] md:w-[440px] lg:w-[520px]"
>
<Magnet padding={150} strength={3}>
<img
src="https://shrug-person-78902957.figma.site/_components/v2/d24c01ad3a56fc65e942a1f501eb73db42d7cf9a/Rectangle_40443.81459862.png"
alt="Jack 3D Creator Portrait"
className="w-full h-auto object-cover"
/>
</Magnet>
</FadeIn>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate HeroSection and FadeIn definitions"
fd -a 'HeroSection\.tsx|FadeIn\.(tsx|ts|jsx|js)$' . | sed 's#^\./##'

echo
echo "Show HeroSection lines 1-120"
if [ -f src/sections/HeroSection.tsx ]; then
  cat -n src/sections/HeroSection.tsx | sed -n '1,120p'
fi

echo
echo "Find FadeIn implementations/usages"
rg -n "function FadeIn|const FadeIn|FadeIn =|export .*FadeIn|FadeIn" --glob '!node_modules' --glob '!dist' --glob '!build' .

Repository: SayanthRock/SayanthRock.github.io

Length of output: 5698


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Show FadeIn implementation"
cat -n src/components/FadeIn.tsx | sed -n '1,140p'

echo
echo "Dependencies mentioning Framer Motion"
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    if [ "$f" = "package.json" ]; then
      jq -r '.dependencies, .devDependencies | to_entries[]? | select(.key=="framer-motion" or .key=="react") | "\(.key)=\(.value)"' "$f"
    else
      rg -n "framer-motion|React" "$f" | head -80
    fi
  fi
done

Repository: SayanthRock/SayanthRock.github.io

Length of output: 1549


🌐 Web query:

Framer Motion animate opacity transform y class inline transform reset animate value transform

💡 Result:

Framer Motion animates opacity, transform, and other properties by applying inline styles to the component. Because it manages these styles dynamically, it can lead to situations where inline transform styles persist or override existing CSS classes. ### How Framer Motion Handles Transforms Framer Motion treats properties like x, y, scale, and rotate as special, independent transform shortcuts [1][2][3]. When you animate these, Framer Motion automatically generates a transform string (e.g., translateX(...) translateY(...)) and applies it as an inline style to the element [4][5]. Because this inline transform style has higher specificity than CSS classes, it may overwrite or conflict with any transform properties defined in your stylesheet (such as transform: translateX(-50%) translateY(-50%) for centering) [6]. ### Addressing Conflicts and Resets If Framer Motion's automatic transform application interferes with your existing CSS: 1. Use transformTemplate: You can use this prop to manually define the output of the transform string, allowing you to preserve existing CSS transforms while injecting animated values [6]. jsx <motion.div animate={{ y: 100 }} transformTemplate={(latest, generated) => `translate(-50%, -50%) ${generated}`} /> 2. Move CSS Transforms to Variants: If an element is centered via CSS transforms, remove that transform from your CSS and include it in your Framer Motion variants or animate prop for all states (initial, animate, exit) to ensure the layout remains consistent [6]. ### Clearing Inline Styles If you need to remove inline styles after an animation completes: * Use transitionEnd: This allows you to define styles that should be applied once the animation finishes [4][7]. jsx <motion.div animate={{ opacity: 1, x: 0 }} transitionEnd={{ transform: 'none' }} /> * Alternative Workaround: In some cases, simply setting the property to its neutral value (e.g., x: 0, y: 0) within the animate object or transitionEnd is sufficient to clean up the transform state [4][7]. ### Performance Note Animating transform and opacity is highly performant because these properties are handled by the browser's compositor thread (GPU acceleration), bypassing the main thread [8]. When possible, prefer using x/y over absolute positioning (top/left) for movement to keep animations smooth and GPU-accelerated [3][8].

Citations:


Move the CSS centering transform off the Framer Motion element.

FadeIn applies Framer Motion x/y transforms as inline styles on this element, which override the Tailwind -translate-x-1/2 centering class and keep the portrait from staying on left-1/2.

Wrap the faded/magnetized content in a wrapper that owns the left-1/2 -translate-x-1/2 ... centering classes, and let FadeIn animate the inner element without owning the layout transform.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sections/HeroSection.tsx` around lines 30 - 43, Move the absolute
positioning and centering classes from the FadeIn element to a wrapper around
the faded/magnetized content, keeping left-1/2 and -translate-x-1/2 on that
wrapper. Leave FadeIn to animate the inner portrait without conflicting layout
transforms, while preserving the existing responsive positioning and sizing
behavior.

Comment on lines 35 to 68
useEffect(() => {
const handleScroll = () => {
if (!sectionRef.current) return;
let animationFrameId: number;
let lastScrollY = window.scrollY;

const updateScroll = () => {
if (!sectionRef.current || !row1Ref.current || !row2Ref.current) return;

const sectionTop = sectionRef.current.offsetTop;
const newOffset = (window.scrollY - sectionTop + window.innerHeight) * 0.3;
setOffset(newOffset);
const scrollY = window.scrollY;

// Calculate scroll offset based on instructions: (window.scrollY - sectionTop + window.innerHeight) * 0.3
const offset = (scrollY - sectionTop + window.innerHeight) * 0.3;

row1Ref.current.style.transform = `translate3d(${offset - 200}px, 0, 0)`;
row2Ref.current.style.transform = `translate3d(${-(offset - 200)}px, 0, 0)`;

lastScrollY = scrollY;
};

const onScroll = () => {
if (Math.abs(window.scrollY - lastScrollY) > 0) {
animationFrameId = requestAnimationFrame(updateScroll);
}
};

window.addEventListener('scroll', handleScroll, { passive: true });
// Initial call to set correct position on load
handleScroll();
window.addEventListener('scroll', onScroll, { passive: true });
// Initial setup
updateScroll();

return () => window.removeEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', onScroll);
cancelAnimationFrame(animationFrameId);
};
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Fix the ineffective scroll-throttle guard; it lets redundant updateScroll calls stack up per frame.

The guard at line 55, Math.abs(window.scrollY - lastScrollY) > 0, is true on almost every fired scroll event, because lastScrollY only updates when updateScroll runs asynchronously inside requestAnimationFrame (line 51), while native scroll events already only fire when scrollY changes. So this check does not gate anything.

Because animationFrameId is reassigned on every onScroll call without canceling the prior pending request (line 56), multiple requestAnimationFrame(updateScroll) calls can queue up and all fire on the same repaint when several scroll events land before that frame. Each call re-reads sectionRef.current.offsetTop and writes two style.transform values, so this wastes CPU on a hot path during fast or momentum scrolling. It also means cancelAnimationFrame(animationFrameId) in cleanup (line 66) can only cancel the last queued frame, not earlier ones.

Use the standard rAF-throttle pattern: gate scheduling with a flag or null check instead of comparing scroll positions.

⚡ Proposed fix using the standard ticking-flag pattern
   useEffect(() => {
-    let animationFrameId: number;
-    let lastScrollY = window.scrollY;
+    let animationFrameId: number | null = null;

     const updateScroll = () => {
       if (!sectionRef.current || !row1Ref.current || !row2Ref.current) return;

       const sectionTop = sectionRef.current.offsetTop;
       const scrollY = window.scrollY;

       // Calculate scroll offset based on instructions: (window.scrollY - sectionTop + window.innerHeight) * 0.3
       const offset = (scrollY - sectionTop + window.innerHeight) * 0.3;

       row1Ref.current.style.transform = `translate3d(${offset - 200}px, 0, 0)`;
       row2Ref.current.style.transform = `translate3d(${-(offset - 200)}px, 0, 0)`;

-      lastScrollY = scrollY;
+      animationFrameId = null;
     };

     const onScroll = () => {
-      if (Math.abs(window.scrollY - lastScrollY) > 0) {
+      if (animationFrameId === null) {
         animationFrameId = requestAnimationFrame(updateScroll);
       }
     };

     window.addEventListener('scroll', onScroll, { passive: true });
     // Initial setup
     updateScroll();

     return () => {
       window.removeEventListener('scroll', onScroll);
-      cancelAnimationFrame(animationFrameId);
+      if (animationFrameId !== null) cancelAnimationFrame(animationFrameId);
     };
   }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sections/MarqueeSection.tsx` around lines 35 - 68, Replace the
ineffective scroll-position check in the MarqueeSection useEffect with an rAF
scheduling guard, such as a ticking flag or null check, so onScroll queues at
most one updateScroll callback until the pending frame runs. Clear the guard
inside the scheduled callback, preserve the initial updateScroll call, and
ensure cleanup cancels any pending frame and removes the listener.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files zenable/risk:low Zenable assessed this PR as LOW risk.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants