Compare commits

..

2 Commits

Author SHA1 Message Date
BunyodL
8c2b92d194 update: get counter values from backend 2025-05-22 09:26:52 +05:00
BunyodL
6e3a498d46 refactor: make use-intersection-observer hook 2025-05-22 08:53:50 +05:00
3 changed files with 121 additions and 107 deletions

View File

@ -1,77 +1,66 @@
'use client';
import { Users } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import { useIntersectionObserver } from '../hooks/use-intersection-observer';
import AnimatedCounter from './animated-counter';
export default function AboutCounter() {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLDivElement>(null);
const { t } = useTextController();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
const stats = [
{
value: 'about.stats.items.2.value',
suffix: 'about.stats.items.2.suffix',
label: 'about.stats.items.2.label',
},
{
threshold: 0.1,
value: 'about.stats.items.4.value',
suffix: 'about.stats.items.4.suffix',
label: 'about.stats.items.4.label',
},
);
{
value: 'about.stats.items.5.value',
suffix: 'about.stats.items.5.suffix',
label: 'about.stats.items.5.label',
decimals: 1,
},
];
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
export default function AboutCounter() {
const { t } = useTextController();
const { sectionRef, isVisible } = useIntersectionObserver<HTMLDivElement>();
return () => {
observer.disconnect();
const toNumber = (value: string) => {
return Number(t(value));
};
}, []);
return (
<div
ref={sectionRef}
className='my-4 grid grid-cols-1 gap-6 text-center md:my-8'
>
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Users className='h-6 w-6 text-red-600' />
</div>
<h3 className='text-2xl font-bold text-gray-900'>
{isVisible ? <AnimatedCounter end={150} suffix='+' /> : '0+'}
</h3>
<p className='text-gray-600'>{t('about.stats.items.2.label')}</p>
</div>
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Users className='h-6 w-6 text-red-600' />
</div>
<h3 className='text-2xl font-bold text-gray-900'>
{isVisible ? <AnimatedCounter end={5} suffix='M+' /> : '0M+'}
</h3>
<p className='text-gray-600'>{t('about.stats.items.4.label')}</p>
</div>
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
{stats.map((stat, index) => (
<div
key={index}
className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'
>
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
<Users className='h-6 w-6 text-red-600' />
</div>
<h3 className='text-2xl font-bold text-gray-900'>
{isVisible ? (
<AnimatedCounter end={98} suffix='%' decimals={1} />
<AnimatedCounter
end={toNumber(stat.value)}
suffix={t(stat.suffix)}
decimals={stat.decimals || 0}
/>
) : (
'0%'
`0${t(stat.suffix)}`
)}
</h3>
<p className='text-gray-600'>{t('about.stats.items.5.label')}</p>
<p className='text-gray-600'>{t(stat.label)}</p>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,31 @@
import { useEffect, useRef, useState } from 'react';
export const useIntersectionObserver = <T extends HTMLElement>() => {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<T>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
threshold: 0.1,
},
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return { sectionRef, isVisible };
};

View File

@ -1,69 +1,63 @@
'use client';
import { useEffect, useRef, useState } from 'react';
import { Container } from '@/shared/components/container';
import { useIntersectionObserver } from '@/shared/hooks/use-intersection-observer';
import { useTextController } from '@/shared/language/hooks/use-text-controller';
import AnimatedCounter from '../shared/components/animated-counter';
export function StatsSection() {
const [isVisible, setIsVisible] = useState(false);
const sectionRef = useRef<HTMLDivElement>(null);
const { t } = useTextController();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
const stats = [
{
key: 'stations',
value: 'home.stats.stations.value',
suffix: 'home.stats.stations.suffix',
label: 'home.stats.stations',
},
{
threshold: 0.1,
key: 'daily',
value: 'home.stats.daily.value',
suffix: 'home.stats.daily.suffix',
label: 'home.stats.daily',
},
);
{
key: 'years',
value: 'home.stats.years.value',
suffix: '',
label: 'home.stats.years',
},
{
key: 'mode',
value: 'home.stats.mode.value',
suffix: 'home.stats.mode.suffix',
label: 'home.stats.mode',
},
];
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
export function StatsSection() {
const { t } = useTextController();
const { sectionRef, isVisible } = useIntersectionObserver<HTMLDivElement>();
return () => {
observer.disconnect();
};
}, []);
const toNumber = (value: string) => Number(t(value));
return (
<section ref={sectionRef} className='bg-red-600 text-white'>
<Container>
<div className='grid grid-cols-2 gap-4 text-center sm:gap-8 md:grid-cols-4'>
<div className='space-y-2'>
{stats.map(({ key, value, suffix, label }) => (
<div key={key} className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={25} suffix='+' /> : '0+'}
{isVisible ? (
<AnimatedCounter
end={toNumber(value)}
suffix={t(suffix) || undefined}
/>
) : (
`0${t(suffix) || ''}`
)}
</h3>
<p className='text-sm text-white/80'>{t('home.stats.stations')}</p>
</div>
<div className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={10000} suffix='+' /> : '0+'}
</h3>
<p className='text-sm text-white/80'>{t('home.stats.daily')}</p>
</div>
<div className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={15} /> : '0'}
</h3>
<p className='text-sm text-white/80'>{t('home.stats.years')}</p>
</div>
<div className='space-y-2'>
<h3 className='text-3xl font-bold'>
{isVisible ? <AnimatedCounter end={24} suffix='/7' /> : '0/7'}
</h3>
<p className='text-sm text-white/80'>{t('home.stats.mode')}</p>
<p className='text-sm text-white/80'>{t(label)}</p>
</div>
))}
</div>
</Container>
</section>