Skip to content

Commit ac2c4e8

Browse files
committed
fix(animations): add countup animation to admins page and make it reusable component
1 parent 5fc7d19 commit ac2c4e8

File tree

3 files changed

+82
-35
lines changed

3 files changed

+82
-35
lines changed

dashboard/src/components/AdminStatistics.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import useDirDetection from '@/hooks/use-dir-detection'
22
import { cn } from '@/lib/utils.ts'
3-
import { numberWithCommas } from '@/utils/formatByte'
43
import { useTranslation } from 'react-i18next'
54
import { Card, CardTitle } from '@/components/ui/card'
5+
import { CountUp } from '@/components/ui/count-up'
66
import { type AdminDetails } from '@/service/api'
77
import { User, UserCheck, UserX } from 'lucide-react'
8-
import React from 'react'
8+
import React, { useEffect, useState } from 'react'
99

1010
interface AdminsStatisticsProps {
1111
data: AdminDetails[]
@@ -14,28 +14,48 @@ interface AdminsStatisticsProps {
1414
export default function AdminStatisticsSection({ data }: AdminsStatisticsProps) {
1515
const { t } = useTranslation()
1616
const dir = useDirDetection()
17+
const [prevStats, setPrevStats] = useState<{ total: number; active: number; disabled: number } | null>(null)
18+
const [isIncreased, setIsIncreased] = useState<Record<string, boolean>>({})
19+
1720
const total = data.length
1821
const disabled = data.filter(a => a.is_disabled).length
1922
const active = total - disabled
2023

24+
const currentStats = { total, active, disabled }
25+
26+
useEffect(() => {
27+
if (prevStats) {
28+
setIsIncreased({
29+
total: currentStats.total > prevStats.total,
30+
active: currentStats.active > prevStats.active,
31+
disabled: currentStats.disabled > prevStats.disabled,
32+
})
33+
}
34+
setPrevStats(currentStats)
35+
// eslint-disable-next-line react-hooks/exhaustive-deps
36+
}, [data])
37+
2138
const stats = [
2239
{
2340
icon: User,
2441
label: t('admins.total'),
2542
value: total,
2643
color: '',
44+
key: 'total',
2745
},
2846
{
2947
icon: UserCheck,
3048
label: t('admins.active'),
3149
value: active,
3250
color: '',
51+
key: 'active',
3352
},
3453
{
3554
icon: UserX,
3655
label: t('admins.disable'),
3756
value: disabled,
3857
color: '',
58+
key: 'disabled',
3959
},
4060
]
4161

@@ -64,8 +84,15 @@ export default function AdminStatisticsSection({ data }: AdminsStatisticsProps)
6484
{React.createElement(stat.icon, { className: 'h-6 w-6' })}
6585
<span>{stat.label}</span>
6686
</div>
67-
<span className="text-3xl font-bold" dir="ltr">
68-
{numberWithCommas(stat.value)}
87+
<span
88+
className={cn(
89+
'mx-2 text-3xl font-bold transition-all duration-500',
90+
isIncreased[stat.key] ? 'animate-zoom-out' : ''
91+
)}
92+
style={{ animationDuration: '400ms' }}
93+
dir="ltr"
94+
>
95+
<CountUp end={stat.value} />
6996
</span>
7097
</CardTitle>
7198
</Card>

dashboard/src/components/UsersStatistics.tsx

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,11 @@
11
import useDirDetection from '@/hooks/use-dir-detection'
22
import { cn } from '@/lib/utils'
33
import { useGetSystemStats } from '@/service/api'
4-
import { numberWithCommas } from '@/utils/formatByte'
54
import { Users, Wifi } from 'lucide-react'
65
import { useEffect, useState } from 'react'
76
import { useTranslation } from 'react-i18next'
87
import { Card, CardTitle } from './ui/card'
9-
10-
const CountUp = ({ end, duration = 1500 }: { end: number; duration?: number }) => {
11-
const [count, setCount] = useState(0)
12-
13-
useEffect(() => {
14-
if (!end) return
15-
16-
let startTimestamp: number | null = null
17-
const startValue = count
18-
const step = (timestamp: number) => {
19-
if (!startTimestamp) startTimestamp = timestamp
20-
const progress = Math.min((timestamp - startTimestamp) / duration, 1)
21-
// Using easeOutQuad for a softer animation
22-
const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2
23-
const currentCount = Math.floor(eased * (end - startValue) + startValue)
24-
25-
setCount(currentCount)
26-
27-
if (progress < 1) {
28-
window.requestAnimationFrame(step)
29-
} else {
30-
setCount(end)
31-
}
32-
}
33-
34-
window.requestAnimationFrame(step)
35-
}, [end, duration])
36-
37-
return <>{numberWithCommas(count)}</>
38-
}
8+
import { CountUp } from './ui/count-up'
399

4010
const UsersStatistics = () => {
4111
const { t } = useTranslation()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useEffect, useState } from 'react'
2+
import { numberWithCommas } from '@/utils/formatByte'
3+
4+
interface CountUpProps {
5+
end: number
6+
duration?: number
7+
}
8+
9+
export const CountUp = ({ end, duration = 800 }: CountUpProps) => {
10+
const [count, setCount] = useState(0)
11+
12+
useEffect(() => {
13+
if (!end && end !== 0) {
14+
setCount(0)
15+
return
16+
}
17+
18+
let startTimestamp: number | null = null
19+
const startValue = count
20+
let animationFrameId: number | null = null
21+
22+
const step = (timestamp: number) => {
23+
if (!startTimestamp) startTimestamp = timestamp
24+
const progress = Math.min((timestamp - startTimestamp) / duration, 1)
25+
// Using easeOutQuad for a softer animation
26+
const eased = progress < 0.5 ? 2 * progress * progress : 1 - Math.pow(-2 * progress + 2, 2) / 2
27+
const currentCount = Math.floor(eased * (end - startValue) + startValue)
28+
29+
setCount(currentCount)
30+
31+
if (progress < 1) {
32+
animationFrameId = window.requestAnimationFrame(step)
33+
} else {
34+
setCount(end)
35+
}
36+
}
37+
38+
animationFrameId = window.requestAnimationFrame(step)
39+
40+
return () => {
41+
if (animationFrameId !== null) {
42+
window.cancelAnimationFrame(animationFrameId)
43+
}
44+
}
45+
// eslint-disable-next-line react-hooks/exhaustive-deps
46+
}, [end, duration])
47+
48+
return <>{numberWithCommas(count)}</>
49+
}
50+

0 commit comments

Comments
 (0)