Animate home stats numbers as count-up 0 to final (Task #25)

The home "إحسانكم لعام 2026" stats numbers now count up from zero to their
final value when the section scrolls into view, like a counter.

New component (artifacts/ehsan-poc/src/components/CountUp.tsx):
- Parses the formatted value string: first space-separated token is the
  number, the rest is the suffix (مليون/ألف/مليار / Million/Thousand/Billion).
- Detects the decimal separator present in the token (comma for AR "85,4",
  period for "3.113"/EN) and the decimal-digit count; parses to float by
  normalizing the separator to ".".
- Uses framer-motion useInView (once, amount 0.3) to trigger a
  requestAnimationFrame ease-out animation (~1.6s) from 0 -> target,
  re-formatting each frame with the same decimals + separator + suffix.
- Respects useReducedMotion: renders final value immediately.
- Safe fallback: if the token has no parseable number, renders the original
  string unchanged.

Wire-up (artifacts/ehsan-poc/src/pages/home.tsx):
- Destructured `language` from useLanguage.
- Replaced raw {value} in the stat number div with
  <CountUp key={`${language}-${value}`} value={value} /> so it re-parses and
  re-animates on AR<->EN toggle. Color (#14573A), size, layout unchanged.

Verified: tsc --noEmit clean; screenshot shows numbers mid-animation with
correct separators/suffixes in AR (RTL).
This commit is contained in:
Replit Agent
2026-06-05 21:09:21 +00:00
parent 844f0f77ac
commit 464e42aaa1
2 changed files with 88 additions and 2 deletions
+3 -2
View File
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { OpportunityCard } from "../components/OpportunityCard";
import { Reveal } from "../components/Reveal";
import { CountUp } from "../components/CountUp";
import { Link } from "wouter";
import { Pause, Play, Leaf, HandHeart, Users, Wallet } from "lucide-react";
import zakatBanner from "@assets/zakatbanarWEB_1780682994527.png";
@@ -41,7 +42,7 @@ function Ornament({ className = "" }: { className?: string }) {
}
export default function Home() {
const { t } = useLanguage();
const { t, language } = useLanguage();
const { data: published, isLoading: pubLoading } = useListPublishedRequests();
const [slide, setSlide] = useState(0);
const [paused, setPaused] = useState(false);
@@ -201,7 +202,7 @@ export default function Home() {
<CardContent className="p-8 text-center">
<Icon className="w-9 h-9 text-primary mx-auto mb-4" aria-hidden="true" />
<div className="text-3xl font-bold text-[#14573A] mb-3" data-testid={`stat-value-${i}`}>
{value}
<CountUp key={`${language}-${value}`} value={value} />
</div>
<div className="text-sm text-muted-foreground font-medium">{label}</div>
</CardContent>