Skip to content

WIP: Restyling - #15

Open
bprosen wants to merge 3 commits into
mainfrom
dev
Open

bprosen wants to merge 3 commits into
mainfrom
dev

Conversation

@bprosen

@bprosen bprosen commented Jan 24, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Added social media header with GitHub, LinkedIn, and Email links
    • Implemented typing animation for hero text
    • Added full-page scrolling with blur effects for improved visual polish
    • Introduced scroll-down indicator icon
  • UI/UX Updates

    • Refactored layout to center content and improve visual hierarchy
    • Enhanced animations with new animation libraries
  • Dependencies

    • Added icon, animation, and motion libraries for enhanced visual effects

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel

vercel Bot commented Jan 24, 2026 •

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
website Ready Ready Preview, Comment Jan 31, 2026 10:41pm

@coderabbitai

coderabbitai Bot commented Jan 24, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces three npm dependencies (@tabler/icons-react, framer-motion, gsap) and adds several new components (Header, Intro, ScrollDownIndicator, FullPageBlurScroll, TextType) featuring scroll-based animations and typed text effects. The VantaNet background component is removed entirely, and the Home route is refactored from async to synchronous while restructuring its composition.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added @tabler/icons-react, framer-motion, and gsap for animation and icon support.
New Components
src/components/Header/Header.tsx, src/components/Intro/Intro.tsx, src/components/ScrollDownIndicator/ScrollDownIndicator.tsx
Created three new header/intro components: Header with social links, Intro with typed text animation, and ScrollDownIndicator chevron icon.
Full-page Scroll Animation
src/components/FullPageScroll/FullPageScroll.tsx
New FullPageBlurScroll component enabling vertical full-page snap scroll with per-page blur animations driven by visibility detection.
Text Animation
src/ui/TextType.tsx
New comprehensive typewriter component supporting typing/deleting phases, multi-sentence loops, GSAP cursor animation, variable speed, reverse mode, and visibility-triggered start via IntersectionObserver.
Constants
src/constants/urls.ts, src/constants/index.ts
New urls.ts file exporting GITHUB, LINKED_IN, and EMAIL constants; barrel export added to index.
Route Refactor
src/routes/Home.tsx
Converted from async to synchronous; replaced VantaNet + UnderConstruction with Intro and ScrollDownIndicator in vertical layout; removed mobile detection logic.
Page Wrapper Update
src/app/page.tsx
Wrapped Home component inside FullPageBlurScroll with blur prop; added UnderConstruction as second page within scroll container.
Component Layout Refresh
src/components/UnderConstruction/UnderConstruction.tsx
Refactored from margin-based positioning to centered full-page min-h-screen wrapper layout.
Component & UI Exports
src/components/index.ts, src/ui/index.ts
Added exports for Header, Intro, ScrollDownIndicator, FullPageScroll (duplicated), and TextType; removed VantaNet export.
Removed Component
src/ui/VantaNet.tsx
Completely removed VantaNet background effect component (140 lines) including initialization logic, color resolution, Vanta.NET polling, and cleanup handlers.
Configuration
components.json
Added @react-bits registry configuration for component management.

Sequence Diagram

sequenceDiagram
    participant User as User (Scroll)
    participant Page as Page Component
    participant FPS as FullPageBlurScroll
    participant BlurPage as BlurPage (Internal)
    participant InView as useInView Hook
    participant Motion as Framer Motion
    participant Child as Child Page

    User->>Page: Load page
    Page->>FPS: Render children within<br/>FullPageBlurScroll
    FPS->>BlurPage: Wrap each child<br/>in BlurPage component
    BlurPage->>InView: Register with InView<br/>detection (rootRef)
    
    User->>FPS: Scroll to next page
    FPS->>InView: Check page visibility<br/>in scroll container
    alt Page In View
        InView->>Motion: isInView = true
        Motion->>Child: blur animation<br/>removed (opacity 1)
    else Page Out of View
        InView->>Motion: isInView = false
        Motion->>Child: apply blur animation<br/>(opacity reduced)
    end
    Child->>User: Render blurred/clear
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • make mobile better #13: Modifies src/routes/Home.tsx to add mobile-aware VantaNet logic; this PR removes those VantaNet changes and refactors Home composition entirely.
  • Fix types and add animated background #12: Introduces VantaNet component implementation in src/ui/VantaNet.tsx; this PR removes that entire component and replaces it with new animation-driven architecture.
  • Add mobile responsiveness #11: Updates UnderConstruction component layout and styling; this PR also refactors UnderConstruction to a centered full-page wrapper.

Poem

🐰✨ With typing animations and scrolling pages so grand,
Blur effects dance as we scroll through the land,
No more Vanta swirls—now we animate with flair,
Framer and GSAP spin magic through the air! 💫🎬

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'WIP: Restyling' is vague and non-descriptive. It fails to convey the specific, meaningful changes in the changeset, which includes major refactoring (Header, Intro, ScrollDownIndicator components), animation additions (TextType, FullPageBlurScroll), dependency updates, and Home route restructuring. Replace with a more descriptive title that captures the primary changes, such as 'Add Header, Intro, and ScrollDownIndicator components with animation support' or 'Refactor Home page layout and add new UI components'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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 and usage tips.

@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: 2

🤖 Fix all issues with AI agents
In `@src/components/Header/Header.tsx`:
- Around line 14-22: Add accessible names and security attributes to the
external icon links: for the GITHUB and LINKED_IN anchor elements (wrapping
IconBrandGithub and IconBrandLinkedin) add descriptive aria-label attributes
(e.g., "GitHub profile", "LinkedIn profile") and include rel="noopener
noreferrer" alongside target="_blank"; for the EMAIL anchor (wrapping IconMail
with href using EMAIL/mailto:) remove target="_blank" (or avoid adding it) and
add an aria-label like "Email" so screen readers announce it. Ensure the
attributes are applied on the anchor elements that reference the GITHUB,
LINKED_IN, and EMAIL constants.

In `@src/routes/Home.tsx`:
- Around line 18-21: The content wrapper uses className 'h-screen' which
together with the separate <Header /> causes vertical overflow; update the div
containing UnderConstruction to use flexible growth instead (e.g., replace
'h-screen' with 'flex-1' or 'min-h-0' and ensure a parent container uses 'flex
flex-col' so the content can grow/shrink) — change the className on the div that
wraps UnderConstruction accordingly to avoid forcing a full viewport height
while keeping centered layout.
🧹 Nitpick comments (3)
package.json (1)

32-32: Consider consolidating icon libraries.

The project now uses both lucide-react (line 40) and @tabler/icons-react. While both are excellent libraries, using two icon libraries increases bundle size. Consider whether one library could fulfill all icon needs, or if there's a specific reason for using both.

src/components/AnimatedSignature/AnimatedSignature.tsx (1)

5-9: Consider providing default values for optional props.

The duration and delay props are optional but passed directly to transition. Consider providing sensible defaults to make the component behavior more predictable.

Suggested defaults
-const MotionPath = ({ d, duration, delay }: MotionPathProps) => {
+const MotionPath = ({ d, duration = 0.3, delay = 0 }: MotionPathProps) => {
src/components/Header/Header.tsx (1)

9-25: Consider adding hover states for better interactivity.

The social links could benefit from hover/focus states to improve user feedback. This can be achieved with Tailwind classes.

Example hover state
-        <a href={GITHUB} target='_blank' rel='noopener noreferrer' aria-label='GitHub'>
+        <a href={GITHUB} target='_blank' rel='noopener noreferrer' aria-label='GitHub' className='hover:opacity-80 transition-opacity'>

Comment on lines +14 to +22
<a href={GITHUB} target='_blank'>
<IconBrandGithub color='#FFFFFF' size={40} stroke={1.2} />
</a>
<a href={LINKED_IN} target='_blank'>
<IconBrandLinkedin color='#FFFFFF' size={40} stroke={1.2} />
</a>
<a href={EMAIL} target='_blank'>
<IconMail color='#FFFFFF' size={40} stroke={1.2} />
</a>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add accessible labels and rel="noopener noreferrer" to external links.

The icon-only links lack accessible names, making them unannounced to screen reader users. Additionally, target="_blank" links should include rel="noopener noreferrer" as a security best practice.

Proposed fix
-        <a href={GITHUB} target='_blank'>
+        <a href={GITHUB} target='_blank' rel='noopener noreferrer' aria-label='GitHub'>
           <IconBrandGithub color='#FFFFFF' size={40} stroke={1.2} />
         </a>
-        <a href={LINKED_IN} target='_blank'>
+        <a href={LINKED_IN} target='_blank' rel='noopener noreferrer' aria-label='LinkedIn'>
           <IconBrandLinkedin color='#FFFFFF' size={40} stroke={1.2} />
         </a>
-        <a href={EMAIL} target='_blank'>
+        <a href={EMAIL} rel='noopener noreferrer' aria-label='Email'>
           <IconMail color='#FFFFFF' size={40} stroke={1.2} />
         </a>

Note: The mailto: link doesn't need target="_blank" since it opens the email client rather than a new browser tab.

📝 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
<a href={GITHUB} target='_blank'>
<IconBrandGithub color='#FFFFFF' size={40} stroke={1.2} />
</a>
<a href={LINKED_IN} target='_blank'>
<IconBrandLinkedin color='#FFFFFF' size={40} stroke={1.2} />
</a>
<a href={EMAIL} target='_blank'>
<IconMail color='#FFFFFF' size={40} stroke={1.2} />
</a>
<a href={GITHUB} target='_blank' rel='noopener noreferrer' aria-label='GitHub'>
<IconBrandGithub color='#FFFFFF' size={40} stroke={1.2} />
</a>
<a href={LINKED_IN} target='_blank' rel='noopener noreferrer' aria-label='LinkedIn'>
<IconBrandLinkedin color='#FFFFFF' size={40} stroke={1.2} />
</a>
<a href={EMAIL} rel='noopener noreferrer' aria-label='Email'>
<IconMail color='#FFFFFF' size={40} stroke={1.2} />
</a>
🤖 Prompt for AI Agents
In `@src/components/Header/Header.tsx` around lines 14 - 22, Add accessible names
and security attributes to the external icon links: for the GITHUB and LINKED_IN
anchor elements (wrapping IconBrandGithub and IconBrandLinkedin) add descriptive
aria-label attributes (e.g., "GitHub profile", "LinkedIn profile") and include
rel="noopener noreferrer" alongside target="_blank"; for the EMAIL anchor
(wrapping IconMail with href using EMAIL/mailto:) remove target="_blank" (or
avoid adding it) and add an aria-label like "Email" so screen readers announce
it. Ensure the attributes are applied on the anchor elements that reference the
GITHUB, LINKED_IN, and EMAIL constants.

Comment thread src/routes/Home.tsx Outdated
@bprosen bprosen changed the title WIP: Header WIP: Restyling Jan 31, 2026

@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: 3

🤖 Fix all issues with AI agents
In `@src/components/Intro/Intro.tsx`:
- Around line 10-15: In Intro.tsx update the TextType usage: fix the className
prop by replacing the invalid Tailwind utility "text-bold" with "font-bold" and
correct the textColors prop by removing or replacing the invalid hex "#jfgwF89"
(contains non-hex chars) with a valid color (e.g., a proper 6- or 3-digit hex
like "#ff8800" or a named color). Ensure these changes are applied on the
TextType component invocation so className and textColors are valid.

In `@src/ui/TextType.tsx`:
- Around line 114-191: The effect and cursor logic assume
textArray[currentTextIndex] exists; add a guard at the top of the useEffect to
return early when textArray.length === 0 (or when currentTextIndex is out of
bounds) so no access to currentText occurs, and update the shouldHideCursor
computation to safely reference textArray only when length > 0; adjust code
paths that rely on processedText/currentText to handle the empty-array case
(e.g., skip animations, avoid setting timeouts, and no calls to
onSentenceComplete) using the existing symbols currentTextIndex, textArray,
processedText, displayedText, shouldHideCursor, and the useEffect cleanup logic.
- Around line 101-112: The useEffect that creates the cursor tween (referencing
cursorRef, showCursor, cursorBlinkDuration and gsap) lacks GSAP context and
cleanup; wrap the animation code in const ctx = gsap.context(() => { ... }) so
the gsap.set and gsap.to live inside that context, and return a cleanup function
from useEffect that calls ctx.revert() to kill the tween and revert styles on
unmount or dependency change; keep the same dependency array ([showCursor,
cursorBlinkDuration]) so the animation is recreated correctly when needed.
🧹 Nitpick comments (3)
src/components/FullPageScroll/FullPageScroll.tsx (2)

21-31: Honor prefers-reduced-motion for blur transitions.

Consider disabling the blur animation when users request reduced motion to avoid discomfort.

♿ Proposed adjustment
-import { motion, useInView } from 'framer-motion';
+import { motion, useInView, useReducedMotion } from 'framer-motion';
@@
 const BlurPage = ({ children, scrollRef, blur }: BlurPageProps) => {
   const pageRef = useRef<HTMLDivElement>(null);
+  const prefersReducedMotion = useReducedMotion();
   const isInView = useInView(pageRef, {
     amount: 0.5,
     root: scrollRef,
   });
@@
-      animate={{
-        filter: blur && !isInView ? 'blur(10px)' : 'blur(0px)',
-      }}
+      animate={{
+        filter:
+          blur && !isInView && !prefersReducedMotion
+            ? 'blur(10px)'
+            : 'blur(0px)',
+      }}
@@
-      transition={{
-        duration: 0.5,
-        ease: [0.25, 0.46, 0.45, 0.94],
-        type: 'tween',
-      }}
+      transition={
+        prefersReducedMotion
+          ? { duration: 0 }
+          : {
+              duration: 0.5,
+              ease: [0.25, 0.46, 0.45, 0.94],
+              type: 'tween',
+            }
+      }

61-65: Prefer child keys over index for stable page identity.

Children.toArray preserves keys; using them avoids state resets if pages reorder.

♻️ Proposed update
-import { Children, type ReactNode, type RefObject, useRef } from 'react';
+import {
+  Children,
+  type ReactNode,
+  type RefObject,
+  useRef,
+  isValidElement,
+} from 'react';
@@
-      {pages.map((page, index) => (
-        <BlurPage
-          blur={blur}
-          key={`scroll-page-${index + 1}`}
-          scrollRef={scrollRef}
-        >
-          {page}
-        </BlurPage>
-      ))}
+      {pages.map((page, index) => {
+        const key =
+          isValidElement(page) && page.key != null
+            ? page.key
+            : `scroll-page-${index + 1}`;
+
+        return (
+          <BlurPage blur={blur} key={key} scrollRef={scrollRef}>
+            {page}
+          </BlurPage>
+        );
+      })}
src/components/index.ts (1)

1-2: Remove the duplicate FullPageScroll re-export.

This is redundant and can surface duplicate-export diagnostics in some toolchains.

🧹 Proposed cleanup
-export * from './FullPageScroll/FullPageScroll';
 export * from './FullPageScroll/FullPageScroll';

Comment on lines +10 to +15
<TextType
className='underline underline-offset-4 decoration-2 text-bold'
deletingSpeed={30}
text={['Frontend', 'Backend', 'React', 'Java']}
textColors={['#4720ff', '#4249fD', '#94249F', '#jfgwF89']}
typingSpeed={60}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if file exists and read the relevant section
if [ -f "src/components/Intro/Intro.tsx" ]; then
  echo "=== File exists, reading lines 10-15 ==="
  sed -n '10,15p' "src/components/Intro/Intro.tsx" | cat -n
else
  echo "File not found, searching for Intro.tsx"
  find . -name "Intro.tsx" -type f
fi

Repository: bprosen/Website

Length of output: 428


🏁 Script executed:

# Validate the hex color format
python3 << 'EOF'
import re

color = '#jfgwF89'
# Valid hex colors: # followed by 3, 4, 6, or 8 hex digits (0-9, a-f, A-F)
hex_pattern = r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$'

if re.match(hex_pattern, color):
    print(f"✓ {color} is a VALID hex color")
else:
    print(f"✗ {color} is an INVALID hex color")
    print(f"  Issues: Contains non-hex characters ('j' and 'w' are not valid hex digits)")
EOF

Repository: bprosen/Website

Length of output: 170


🏁 Script executed:

# Look for Tailwind config files
find . -name "tailwind.config.*" -type f

Repository: bprosen/Website

Length of output: 41


🏁 Script executed:

# Search for any definition of text-bold in the codebase
rg "text-bold" --type ts --type tsx --type js --type jsx

Repository: bprosen/Website

Length of output: 86


🏁 Script executed:

# Check if there's a custom CSS or Tailwind setup that might define text-bold
find . -name "*.css" -o -name "globals.css" | head -10

Repository: bprosen/Website

Length of output: 80


🏁 Script executed:

# Search for text-bold across all files
rg "text-bold"

Repository: bprosen/Website

Length of output: 165


🏁 Script executed:

# Read globals.css to check for custom Tailwind config
cat -n "./src/app/globals.css"

Repository: bprosen/Website

Length of output: 5366


🏁 Script executed:

# Also check if there's a tailwind config in root
ls -la | grep -i tailwind

Repository: bprosen/Website

Length of output: 41


🌐 Web query:

Tailwind CSS text-bold font-bold utility class

💡 Result:

Tailwind CSS doesn’t have a text-bold utility. To make text bold, use font-bold, which sets font-weight: 700. (v3.tailwindcss.com)

<p class="font-bold">Bold text</p>

Related weights include font-semibold, font-extrabold, etc. (v3.tailwindcss.com)

Citations:


Replace invalid Tailwind class and remove invalid color value.
text-bold is not a Tailwind utility; use font-bold instead. The hex color #jfgwF89 is invalid (contains non-hex characters 'j' and 'w').

Suggested fix
-          <TextType
-            className='underline underline-offset-4 decoration-2 text-bold'
+          <TextType
+            className='underline underline-offset-4 decoration-2 font-bold'
             deletingSpeed={30}
             text={['Frontend', 'Backend', 'React', 'Java']}
-            textColors={['#4720ff', '#4249fD', '#94249F', '#jfgwF89']}
+            textColors={['#4720ff', '#4249fD', '#94249F']}
             typingSpeed={60}
           />
📝 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
<TextType
className='underline underline-offset-4 decoration-2 text-bold'
deletingSpeed={30}
text={['Frontend', 'Backend', 'React', 'Java']}
textColors={['#4720ff', '#4249fD', '#94249F', '#jfgwF89']}
typingSpeed={60}
<TextType
className='underline underline-offset-4 decoration-2 font-bold'
deletingSpeed={30}
text={['Frontend', 'Backend', 'React', 'Java']}
textColors={['#4720ff', '#4249fD', '#94249F']}
typingSpeed={60}
/>
🤖 Prompt for AI Agents
In `@src/components/Intro/Intro.tsx` around lines 10 - 15, In Intro.tsx update the
TextType usage: fix the className prop by replacing the invalid Tailwind utility
"text-bold" with "font-bold" and correct the textColors prop by removing or
replacing the invalid hex "#jfgwF89" (contains non-hex chars) with a valid color
(e.g., a proper 6- or 3-digit hex like "#ff8800" or a named color). Ensure these
changes are applied on the TextType component invocation so className and
textColors are valid.

Comment thread src/ui/TextType.tsx
Comment on lines +101 to +112
useEffect(() => {
if (showCursor && cursorRef.current) {
gsap.set(cursorRef.current, { opacity: 1 });
gsap.to(cursorRef.current, {
duration: cursorBlinkDuration,
ease: 'power2.inOut',
opacity: 0,
repeat: -1,
yoyo: true,
});
}
}, [showCursor, cursorBlinkDuration]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "TextType.tsx" -type f

Repository: bprosen/Website

Length of output: 80


🏁 Script executed:

cd src/ui && wc -l TextType.tsx && sed -n '95,120p' TextType.tsx

Repository: bprosen/Website

Length of output: 765


🏁 Script executed:

head -120 src/ui/TextType.tsx | tail -30

Repository: bprosen/Website

Length of output: 807


🏁 Script executed:

rg "gsap\." src/ui/TextType.tsx -A 3 -B 1

Repository: bprosen/Website

Length of output: 276


🏁 Script executed:

rg "useEffect" src/ui/TextType.tsx -A 10 | head -80

Repository: bprosen/Website

Length of output: 1162


🌐 Web query:

GSAP tween cleanup kill() unmount memory leak best practices

💡 Result:

GSAP cleanup on unmount (avoid “memory leaks”)

1) Prefer gsap.context() (or React’s useGSAP()) and call revert() in cleanup
This is GSAP’s intended pattern for component lifecycles: anything created inside the context is tracked, then revert() reverts styles AND kills animations/ScrollTriggers, making them eligible for garbage collection. (gsap.com)

React (manual context):

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box", { x: 100, duration: 1 });
  }, ref); // optional scope

  return () => ctx.revert(); // unmount cleanup
}, []);

(gsap.com)

2) If you’re using @gsap/react, rely on useGSAP() + contextSafe() for delayed code
useGSAP() automatically creates a gsap.context() and cleans up recorded GSAP instances. But animations created later (event handlers, timeouts, etc.) won’t be recorded unless wrapped with contextSafe(). Also remove any event listeners in your returned cleanup. (npmjs.com)

3) Know when to use kill() vs revert()

  • animation.kill() stops the animation, removes it from its parent timeline, and releases it for GC (don’t use it if you plan to reuse that animation). (gsap.com)
  • animation.revert() (GSAP 3.11+) restores targets to their pre-animation state (including removing inline styles it added) and kills the animation—often the best “unmount” behavior. (gsap.com)

4) If you used ScrollTrigger, clean it up correctly
ScrollTrigger.kill() removes listeners and can revert pin/spacer DOM changes (revert is true by default), making the instance eligible for GC. (gsap.com)
(If you used gsap.context(), created ScrollTriggers are typically handled for you when you ctx.revert().) (gsap.com)

5) Plugin-specific cleanup: SplitText example
Some plugins require revert() (not just kill()) to restore DOM. For SplitText, kill() stops autosplit listeners but doesn’t restore original HTML; revert() restores and calls kill() internally. (gsap.com)

Rule of thumb: on unmount, use ctx.revert() (or useGSAP()), and additionally clean up your own event listeners/timeouts; only manually kill() individual tweens/ScrollTriggers if you’re not using a context or you created things outside it. (gsap.com)

Citations:


Use gsap.context() with cleanup to prevent lingering cursor animations.

The cursor tween is created without cleanup, allowing animations to persist across component unmounts. GSAP's recommended pattern for React is wrapping animations in gsap.context() and calling ctx.revert() on unmount, which kills the animation and reverts any inline styles.

🧹 Suggested cleanup
  useEffect(() => {
    if (showCursor && cursorRef.current) {
+    const ctx = gsap.context(() => {
       gsap.set(cursorRef.current, { opacity: 1 });
       gsap.to(cursorRef.current, {
         duration: cursorBlinkDuration,
         ease: 'power2.inOut',
         opacity: 0,
         repeat: -1,
         yoyo: true,
       });
+    }, cursorRef);
+    return () => ctx.revert();
     }
  }, [showCursor, cursorBlinkDuration]);
🤖 Prompt for AI Agents
In `@src/ui/TextType.tsx` around lines 101 - 112, The useEffect that creates the
cursor tween (referencing cursorRef, showCursor, cursorBlinkDuration and gsap)
lacks GSAP context and cleanup; wrap the animation code in const ctx =
gsap.context(() => { ... }) so the gsap.set and gsap.to live inside that
context, and return a cleanup function from useEffect that calls ctx.revert() to
kill the tween and revert styles on unmount or dependency change; keep the same
dependency array ([showCursor, cursorBlinkDuration]) so the animation is
recreated correctly when needed.

Comment thread src/ui/TextType.tsx
Comment on lines +114 to +191
useEffect(() => {
if (!isVisible) return;

let timeout: ReturnType<typeof setTimeout>;
const currentText = textArray[currentTextIndex];
const processedText = reverseMode
? currentText.split('').reverse().join('')
: currentText;

const executeTypingAnimation = () => {
if (isDeleting) {
if (displayedText === '') {
setIsDeleting(false);
if (currentTextIndex === textArray.length - 1 && !loop) {
return;
}

if (onSentenceComplete) {
onSentenceComplete(textArray[currentTextIndex], currentTextIndex);
}

setCurrentTextIndex((prev) => (prev + 1) % textArray.length);
setCurrentCharIndex(0);
timeout = setTimeout(() => {}, pauseDuration);
} else {
timeout = setTimeout(() => {
setDisplayedText((prev) => prev.slice(0, -1));
}, deletingSpeed);
}
} else {
if (currentCharIndex < processedText.length) {
timeout = setTimeout(
() => {
setDisplayedText(
(prev) => prev + processedText[currentCharIndex],
);
setCurrentCharIndex((prev) => prev + 1);
},
variableSpeed ? getRandomSpeed() : typingSpeed,
);
} else if (textArray.length >= 1) {
if (!loop && currentTextIndex === textArray.length - 1) return;
timeout = setTimeout(() => {
setIsDeleting(true);
}, pauseDuration);
}
}
};

if (currentCharIndex === 0 && !isDeleting && displayedText === '') {
timeout = setTimeout(executeTypingAnimation, initialDelay);
} else {
executeTypingAnimation();
}

return () => clearTimeout(timeout);
}, [
currentCharIndex,
displayedText,
isDeleting,
typingSpeed,
deletingSpeed,
pauseDuration,
textArray,
currentTextIndex,
loop,
initialDelay,
isVisible,
reverseMode,
variableSpeed,
onSentenceComplete,
getRandomSpeed,
]);

const shouldHideCursor =
hideCursorWhileTyping &&
(currentCharIndex < textArray[currentTextIndex].length || isDeleting);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against empty text arrays to avoid runtime errors.
textArray[currentTextIndex] is assumed to exist; an empty array will crash the effect and cursor logic.

🛡️ Suggested guard
 useEffect(() => {
-  if (!isVisible) return;
+  if (!isVisible || textArray.length === 0) return;
 
   let timeout: ReturnType<typeof setTimeout>;
   const currentText = textArray[currentTextIndex];
@@
-const shouldHideCursor =
-  hideCursorWhileTyping &&
-  (currentCharIndex < textArray[currentTextIndex].length || isDeleting);
+const currentTextLength = textArray[currentTextIndex]?.length ?? 0;
+const shouldHideCursor =
+  hideCursorWhileTyping && (currentCharIndex < currentTextLength || isDeleting);
📝 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(() => {
if (!isVisible) return;
let timeout: ReturnType<typeof setTimeout>;
const currentText = textArray[currentTextIndex];
const processedText = reverseMode
? currentText.split('').reverse().join('')
: currentText;
const executeTypingAnimation = () => {
if (isDeleting) {
if (displayedText === '') {
setIsDeleting(false);
if (currentTextIndex === textArray.length - 1 && !loop) {
return;
}
if (onSentenceComplete) {
onSentenceComplete(textArray[currentTextIndex], currentTextIndex);
}
setCurrentTextIndex((prev) => (prev + 1) % textArray.length);
setCurrentCharIndex(0);
timeout = setTimeout(() => {}, pauseDuration);
} else {
timeout = setTimeout(() => {
setDisplayedText((prev) => prev.slice(0, -1));
}, deletingSpeed);
}
} else {
if (currentCharIndex < processedText.length) {
timeout = setTimeout(
() => {
setDisplayedText(
(prev) => prev + processedText[currentCharIndex],
);
setCurrentCharIndex((prev) => prev + 1);
},
variableSpeed ? getRandomSpeed() : typingSpeed,
);
} else if (textArray.length >= 1) {
if (!loop && currentTextIndex === textArray.length - 1) return;
timeout = setTimeout(() => {
setIsDeleting(true);
}, pauseDuration);
}
}
};
if (currentCharIndex === 0 && !isDeleting && displayedText === '') {
timeout = setTimeout(executeTypingAnimation, initialDelay);
} else {
executeTypingAnimation();
}
return () => clearTimeout(timeout);
}, [
currentCharIndex,
displayedText,
isDeleting,
typingSpeed,
deletingSpeed,
pauseDuration,
textArray,
currentTextIndex,
loop,
initialDelay,
isVisible,
reverseMode,
variableSpeed,
onSentenceComplete,
getRandomSpeed,
]);
const shouldHideCursor =
hideCursorWhileTyping &&
(currentCharIndex < textArray[currentTextIndex].length || isDeleting);
useEffect(() => {
if (!isVisible || textArray.length === 0) return;
let timeout: ReturnType<typeof setTimeout>;
const currentText = textArray[currentTextIndex];
const processedText = reverseMode
? currentText.split('').reverse().join('')
: currentText;
const executeTypingAnimation = () => {
if (isDeleting) {
if (displayedText === '') {
setIsDeleting(false);
if (currentTextIndex === textArray.length - 1 && !loop) {
return;
}
if (onSentenceComplete) {
onSentenceComplete(textArray[currentTextIndex], currentTextIndex);
}
setCurrentTextIndex((prev) => (prev + 1) % textArray.length);
setCurrentCharIndex(0);
timeout = setTimeout(() => {}, pauseDuration);
} else {
timeout = setTimeout(() => {
setDisplayedText((prev) => prev.slice(0, -1));
}, deletingSpeed);
}
} else {
if (currentCharIndex < processedText.length) {
timeout = setTimeout(
() => {
setDisplayedText(
(prev) => prev + processedText[currentCharIndex],
);
setCurrentCharIndex((prev) => prev + 1);
},
variableSpeed ? getRandomSpeed() : typingSpeed,
);
} else if (textArray.length >= 1) {
if (!loop && currentTextIndex === textArray.length - 1) return;
timeout = setTimeout(() => {
setIsDeleting(true);
}, pauseDuration);
}
}
};
if (currentCharIndex === 0 && !isDeleting && displayedText === '') {
timeout = setTimeout(executeTypingAnimation, initialDelay);
} else {
executeTypingAnimation();
}
return () => clearTimeout(timeout);
}, [
currentCharIndex,
displayedText,
isDeleting,
typingSpeed,
deletingSpeed,
pauseDuration,
textArray,
currentTextIndex,
loop,
initialDelay,
isVisible,
reverseMode,
variableSpeed,
onSentenceComplete,
getRandomSpeed,
]);
const currentTextLength = textArray[currentTextIndex]?.length ?? 0;
const shouldHideCursor =
hideCursorWhileTyping && (currentCharIndex < currentTextLength || isDeleting);
🤖 Prompt for AI Agents
In `@src/ui/TextType.tsx` around lines 114 - 191, The effect and cursor logic
assume textArray[currentTextIndex] exists; add a guard at the top of the
useEffect to return early when textArray.length === 0 (or when currentTextIndex
is out of bounds) so no access to currentText occurs, and update the
shouldHideCursor computation to safely reference textArray only when length > 0;
adjust code paths that rely on processedText/currentText to handle the
empty-array case (e.g., skip animations, avoid setting timeouts, and no calls to
onSentenceComplete) using the existing symbols currentTextIndex, textArray,
processedText, displayedText, shouldHideCursor, and the useEffect cleanup logic.

This branch was successfully deployed

1 active deployment
Preview — cd4e32f8 Deployed Jan 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant