Skip to content

Add reusable AnimatedCounter component for stats#2803

Open
pranayukey200 wants to merge 1 commit into
ritesh-1918:mainfrom
pranayukey200:feature/2802-animated-counters
Open

Add reusable AnimatedCounter component for stats#2803
pranayukey200 wants to merge 1 commit into
ritesh-1918:mainfrom
pranayukey200:feature/2802-animated-counters

Conversation

@pranayukey200

@pranayukey200 pranayukey200 commented Jun 12, 2026

Copy link
Copy Markdown

**Feature: Add reusable AnimatedCounter component for stats

This PR extracts the AnimatedStat component from LandingPage.jsx into a proper, reusable AnimatedCounter component at `src/components/AnimatedCounter.jsx, then updates LandingPage.jsx to use the new component! The stats are still animated via IntersectionObserver, only trigger once when visible, use smooth easing and include all original functionality!

Issue #2802 Comment

Hey there! I've implemented the animated counters feature!

I've:

  1. Extracted the AnimatedCounter component into a reusable component at src/components/AnimatedCounter.jsx from LandingPage.jsx
  2. Kept all original functionality (intersection observer, animate once from zero, smooth easing, support for word targets and numbers!
  3. Updated LandingPage to use the new component!
    PR ready for review at: https://github.com/pranayukey200/HELPDESK.AI/pull/new/feature/2802-animated-counters !

Closes #2802!

Summary by CodeRabbit

  • Refactor
    • Created a reusable animated counter component for displaying numeric statistics. The component smoothly counts values when scrolling into view, supporting customizable prefixes, suffixes, and labels. Updated the landing page statistics section to utilize this new component for consistent animation behavior.

@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

@pranayukey200 is attempting to deploy a commit to the ritesh Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new AnimatedCounter React component animates numeric displays from 0 to a target value when at least 50% visible in the viewport, using IntersectionObserver and requestAnimationFrame with cubic ease-out timing over 1500ms. The landing page statistics section is updated to use this reusable component, replacing inline animation logic.

Changes

Animated Counter Feature

Layer / File(s) Summary
AnimatedCounter component with visibility and animation logic
Frontend/src/components/AnimatedCounter.jsx
Component declares props with defaults, initializes display state based on isWord mode, sets up IntersectionObserver to trigger animation when 50%+ visible, and implements requestAnimationFrame loop with cubic ease-out easing to animate from 0 to target over 1500ms. Renders prefix, animated display, and suffix with a label.
Landing page statistics bar migration
Frontend/src/pages/LandingPage.jsx
Imports the new AnimatedCounter component and updates the STATS BAR section to render AnimatedCounter instances for the four statistics, replacing the prior locally-defined AnimatedStat animation logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

type:feature

🐰 A counter born to dance and gleam,
Counts up smoothly like a fuzzy dream,
When viewports scroll, it springs to life,
No more stats dull as hutch knife,
Numbers bound with easing grace!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 accurately summarizes the main change: extracting animation functionality into a new reusable AnimatedCounter component for stats.
Linked Issues check ✅ Passed Code changes fully implement issue #2802 requirements: new AnimatedCounter component with IntersectionObserver viewport detection, smooth animation from 0 to target over 1500ms with easing, support for metrics, and integration into LandingPage stats section.
Out of Scope Changes check ✅ Passed All changes are directly scoped to extracting and implementing the AnimatedCounter component and updating LandingPage to use it; no unrelated modifications are present.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

🧹 Nitpick comments (3)
Frontend/src/components/AnimatedCounter.jsx (3)

47-54: ⚡ Quick win

Add ARIA live region for accessibility.

The animated counter displays dynamic content that changes visually, but screen readers won't announce these updates. Add aria-live="polite" to inform assistive technology users when the count reaches its final value.

♿ Add ARIA attributes
     return (
-        <div ref={ref} className="p-4">
+        <div ref={ref} className="p-4" aria-live="polite" aria-atomic="true">
             <div className="text-4xl font-extrabold mb-1 text-white tabular-nums">
                 {prefix}{display}{suffix}
             </div>
🤖 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 `@Frontend/src/components/AnimatedCounter.jsx` around lines 47 - 54, The
AnimatedCounter component's dynamic numeric output isn't announced to screen
readers; update the return JSX to add an ARIA live region on the element that
renders the changing value (the div with className "text-4xl font-extrabold mb-1
text-white tabular-nums") by adding aria-live="polite" so assistive tech will
announce updates; ensure you modify the AnimatedCounter component's
render/return (the div containing {prefix}{display}{suffix}) to include the
attribute and keep existing refs/props intact.

48-53: 💤 Low value

Consider moving padding to parent for better composability.

The component applies p-4 padding internally, which may conflict with parent layout expectations. Typically, spacing is controlled by the parent container rather than the leaf component, improving reusability across different layouts.

♻️ Remove internal padding
-    return (
-        <div ref={ref} className="p-4">
+    return (
+        <div ref={ref}>
             <div className="text-4xl font-extrabold mb-1 text-white tabular-nums">

Then update the parent in LandingPage.jsx to add padding if needed.

🤖 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 `@Frontend/src/components/AnimatedCounter.jsx` around lines 48 - 53,
AnimatedCounter.jsx currently applies internal padding via the root div's
className="p-4", which reduces composability; remove the "p-4" from the root div
(keep the ref and other classes intact) so the component is padding-agnostic,
then add the needed padding on the parent(s) that render <AnimatedCounter />
(e.g., update LandingPage.jsx containers) to preserve layout where required.

8-24: ⚡ Quick win

Optimize observer effect to avoid recreation.

When triggered changes from false to true, the effect re-runs and creates a new observer that will never trigger again (due to the !triggered guard). Consider restructuring to prevent unnecessary observer recreation.

♻️ Prevent observer recreation after trigger
     useEffect(() => {
         const el = ref.current;
-        if (!el) return;
+        if (!el || triggered) return;
         
         const observer = new IntersectionObserver(
             ([entry]) => {
-                if (entry.isIntersecting && !triggered) {
+                if (entry.isIntersecting) {
                     setTriggered(true);
                 }
             },
             { threshold: 0.5 }
         );
         
         observer.observe(el);
         
         return () => observer.disconnect();
-    }, [triggered]);
+    }, []);
🤖 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 `@Frontend/src/components/AnimatedCounter.jsx` around lines 8 - 24, The effect
recreates the IntersectionObserver when `triggered` changes causing a new
observer that never fires; update the useEffect containing
`IntersectionObserver` (the block referencing `ref.current`, `observer.observe`,
and `observer.disconnect`) to run only once (remove `triggered` from the
dependency array) and either: 1) have the observer callback call
`setTriggered(true)` and then disconnect the observer, or 2) use a mutable ref
(e.g., `triggeredRef.current`) checked/updated inside the callback so the
observer can remain and stop observing once the animation is triggered; ensure
you still call `observer.disconnect()`/`observer.unobserve(el)` when triggered
or on cleanup.
🤖 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 `@Frontend/src/components/AnimatedCounter.jsx`:
- Around line 26-45: The effect's animation uses requestAnimationFrame inside
useEffect (step callback) but never stores or cancels the frame ID, so the
animation can continue after unmount or dependency changes and call setDisplay
on an unmounted component; fix by capturing the returned frame id from
requestAnimationFrame (e.g., frameId = requestAnimationFrame(step)) and return a
cleanup function from useEffect that calls cancelAnimationFrame(frameId) (and
also cancel any pending frame after each requestAnimationFrame call), ensuring
the step/requestAnimationFrame loop is stopped when triggered/target/isWord
change or the component unmounts.
- Line 31: AnimatedCounter.jsx currently does const to = parseFloat(target)
which can produce NaN and render "NaN" if target is non-numeric; update the
AnimatedCounter component to validate target before using it (e.g., attempt
const candidate = parseFloat(target) then if Number.isNaN(candidate) handle
gracefully by logging/warning and using a safe fallback like 0 or early-return),
ensuring you update any downstream code that uses to (the variable assigned from
parseFloat(target)) to rely on the validated value.
- Around line 3-4: The AnimatedCounter component initializes display to target
when isWord is true but never updates it on prop changes because the animated
effect returns early for isWord; add a useEffect in AnimatedCounter that watches
[isWord, target] and, when isWord is true, calls setDisplay(target) so the
displayed word updates whenever the target prop changes (refer to the display
state and the early-returning animation effect).

---

Nitpick comments:
In `@Frontend/src/components/AnimatedCounter.jsx`:
- Around line 47-54: The AnimatedCounter component's dynamic numeric output
isn't announced to screen readers; update the return JSX to add an ARIA live
region on the element that renders the changing value (the div with className
"text-4xl font-extrabold mb-1 text-white tabular-nums") by adding
aria-live="polite" so assistive tech will announce updates; ensure you modify
the AnimatedCounter component's render/return (the div containing
{prefix}{display}{suffix}) to include the attribute and keep existing refs/props
intact.
- Around line 48-53: AnimatedCounter.jsx currently applies internal padding via
the root div's className="p-4", which reduces composability; remove the "p-4"
from the root div (keep the ref and other classes intact) so the component is
padding-agnostic, then add the needed padding on the parent(s) that render
<AnimatedCounter /> (e.g., update LandingPage.jsx containers) to preserve layout
where required.
- Around line 8-24: The effect recreates the IntersectionObserver when
`triggered` changes causing a new observer that never fires; update the
useEffect containing `IntersectionObserver` (the block referencing
`ref.current`, `observer.observe`, and `observer.disconnect`) to run only once
(remove `triggered` from the dependency array) and either: 1) have the observer
callback call `setTriggered(true)` and then disconnect the observer, or 2) use a
mutable ref (e.g., `triggeredRef.current`) checked/updated inside the callback
so the observer can remain and stop observing once the animation is triggered;
ensure you still call `observer.disconnect()`/`observer.unobserve(el)` when
triggered or on cleanup.
🪄 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

Run ID: fdedbe2d-f22e-4565-8cc4-bc2f23fe976f

📥 Commits

Reviewing files that changed from the base of the PR and between da8faf2 and b4be039.

📒 Files selected for processing (2)
  • Frontend/src/components/AnimatedCounter.jsx
  • Frontend/src/pages/LandingPage.jsx

Comment on lines +3 to +4
export default function AnimatedCounter({ target, suffix = '', prefix = '', label, isWord = false }) {
const [display, setDisplay] = useState(isWord ? target : '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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

isWord mode doesn't react to target prop changes.

When isWord is true, display is initialized to target but won't update if the target prop changes after mount. The animation effect (lines 26-45) returns early when isWord is true, so no update mechanism exists.

🔄 Add effect to sync display with target in word mode
     const [display, setDisplay] = useState(isWord ? target : '0');
     const [triggered, setTriggered] = useState(false);
     const ref = useRef(null);
+
+    useEffect(() => {
+        if (isWord) {
+            setDisplay(target);
+        }
+    }, [isWord, target]);
🤖 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 `@Frontend/src/components/AnimatedCounter.jsx` around lines 3 - 4, The
AnimatedCounter component initializes display to target when isWord is true but
never updates it on prop changes because the animated effect returns early for
isWord; add a useEffect in AnimatedCounter that watches [isWord, target] and,
when isWord is true, calls setDisplay(target) so the displayed word updates
whenever the target prop changes (refer to the display state and the
early-returning animation effect).

Comment on lines +26 to +45
useEffect(() => {
if (!triggered || isWord) return;

const duration = 1500;
const start = performance.now();
const to = parseFloat(target);

const step = (now) => {
const progress = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const current = Math.round(to * eased);
setDisplay(String(current));

if (progress < 1) {
requestAnimationFrame(step);
}
};

requestAnimationFrame(step);
}, [triggered, target, isWord]);

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 | 🔴 Critical | ⚡ Quick win

Cancel animation frame on cleanup to prevent memory leak.

The animation effect uses requestAnimationFrame without storing or cancelling the frame ID. If the component unmounts during animation or dependencies change, the animation continues running and updating unmounted state, causing memory leaks and console warnings.

🐛 Store and cancel animation frame in cleanup
     useEffect(() => {
         if (!triggered || isWord) return;
         
         const duration = 1500;
         const start = performance.now();
         const to = parseFloat(target);
+        let rafId;
         
         const step = (now) => {
             const progress = Math.min((now - start) / duration, 1);
             const eased = 1 - Math.pow(1 - progress, 3);
             const current = Math.round(to * eased);
             setDisplay(String(current));
             
             if (progress < 1) {
-                requestAnimationFrame(step);
+                rafId = requestAnimationFrame(step);
             }
         };
         
-        requestAnimationFrame(step);
+        rafId = requestAnimationFrame(step);
+
+        return () => {
+            if (rafId) cancelAnimationFrame(rafId);
+        };
     }, [triggered, target, isWord]);
🤖 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 `@Frontend/src/components/AnimatedCounter.jsx` around lines 26 - 45, The
effect's animation uses requestAnimationFrame inside useEffect (step callback)
but never stores or cancels the frame ID, so the animation can continue after
unmount or dependency changes and call setDisplay on an unmounted component; fix
by capturing the returned frame id from requestAnimationFrame (e.g., frameId =
requestAnimationFrame(step)) and return a cleanup function from useEffect that
calls cancelAnimationFrame(frameId) (and also cancel any pending frame after
each requestAnimationFrame call), ensuring the step/requestAnimationFrame loop
is stopped when triggered/target/isWord change or the component unmounts.


const duration = 1500;
const start = performance.now();
const to = parseFloat(target);

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 | ⚡ Quick win

Validate target before parsing to prevent NaN display.

parseFloat(target) will return NaN if target is not a valid numeric string (e.g., if accidentally passed "Zero" without isWord=true), causing the display to show "NaN" to users.

🛡️ Add validation for numeric target
         const duration = 1500;
         const start = performance.now();
         const to = parseFloat(target);
+        
+        if (isNaN(to)) {
+            console.error(`AnimatedCounter: invalid numeric target "${target}"`);
+            setDisplay(target);
+            return;
+        }
         
         const step = (now) => {
📝 Committable suggestion

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

Suggested change
const to = parseFloat(target);
const duration = 1500;
const start = performance.now();
const to = parseFloat(target);
if (isNaN(to)) {
console.error(`AnimatedCounter: invalid numeric target "${target}"`);
setDisplay(target);
return;
}
const step = (now) => {
🤖 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 `@Frontend/src/components/AnimatedCounter.jsx` at line 31, AnimatedCounter.jsx
currently does const to = parseFloat(target) which can produce NaN and render
"NaN" if target is non-numeric; update the AnimatedCounter component to validate
target before using it (e.g., attempt const candidate = parseFloat(target) then
if Number.isNaN(candidate) handle gracefully by logging/warning and using a safe
fallback like 0 or early-return), ensuring you update any downstream code that
uses to (the variable assigned from parseFloat(target)) to rely on the validated
value.

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.

[Enhancement] Add Animated Counters for Statistics Section

1 participant