Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR introduces three npm dependencies ( Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
durationanddelayprops are optional but passed directly totransition. 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'>
| <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> |
There was a problem hiding this comment.
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.
| <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.
There was a problem hiding this comment.
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: Honorprefers-reduced-motionfor 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.toArraypreserves 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 duplicateFullPageScrollre-export.This is redundant and can surface duplicate-export diagnostics in some toolchains.
🧹 Proposed cleanup
-export * from './FullPageScroll/FullPageScroll'; export * from './FullPageScroll/FullPageScroll';
| <TextType | ||
| className='underline underline-offset-4 decoration-2 text-bold' | ||
| deletingSpeed={30} | ||
| text={['Frontend', 'Backend', 'React', 'Java']} | ||
| textColors={['#4720ff', '#4249fD', '#94249F', '#jfgwF89']} | ||
| typingSpeed={60} |
There was a problem hiding this comment.
🧩 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
fiRepository: 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)")
EOFRepository: bprosen/Website
Length of output: 170
🏁 Script executed:
# Look for Tailwind config files
find . -name "tailwind.config.*" -type fRepository: 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 jsxRepository: 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 -10Repository: 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 tailwindRepository: 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:
- 1: https://v3.tailwindcss.com/docs/font-weight?utm_source=openai
- 2: https://v3.tailwindcss.com/docs/font-weight?utm_source=openai
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.
| <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.
| 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]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "TextType.tsx" -type fRepository: bprosen/Website
Length of output: 80
🏁 Script executed:
cd src/ui && wc -l TextType.tsx && sed -n '95,120p' TextType.tsxRepository: bprosen/Website
Length of output: 765
🏁 Script executed:
head -120 src/ui/TextType.tsx | tail -30Repository: bprosen/Website
Length of output: 807
🏁 Script executed:
rg "gsap\." src/ui/TextType.tsx -A 3 -B 1Repository: bprosen/Website
Length of output: 276
🏁 Script executed:
rg "useEffect" src/ui/TextType.tsx -A 10 | head -80Repository: 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:
- 1: https://gsap.com/docs/v3/GSAP/gsap.context%28%29/?utm_source=openai
- 2: https://gsap.com/docs/v3/GSAP/gsap.context%28%29/?utm_source=openai
- 3: https://www.npmjs.com/package/%40gsap/react/v/1.0.0?utm_source=openai
- 4: https://gsap.com/docs/v3/GSAP/Tween/kill%28%29/?utm_source=openai
- 5: https://gsap.com/docs/v3/GSAP/Timeline/revert%28%29/?utm_source=openai
- 6: https://gsap.com/docs/v3/Plugins/ScrollTrigger/kill%28%29/?utm_source=openai
- 7: https://gsap.com/docs/v3/GSAP/gsap.context%28%29/?utm_source=openai
- 8: https://gsap.com/docs/v3/Plugins/SplitText/kill%28%29/?utm_source=openai
- 9: https://gsap.com/docs/v3/GSAP/gsap.context%28%29/?utm_source=openai
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.
| 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); | ||
|
|
There was a problem hiding this comment.
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.
| 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.
Summary by CodeRabbit
Release Notes
New Features
UI/UX Updates
Dependencies
✏️ Tip: You can customize this high-level summary in your review settings.