Compare commits
No commits in common. "8c2b92d194fb5a88b5ba0edeaf2216625521a8dc" and "cdc9de27f3cc9a343f8d3b35255240c12dbb83d7" have entirely different histories.
8c2b92d194
...
cdc9de27f3
@ -1,66 +1,77 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
import { useIntersectionObserver } from '../hooks/use-intersection-observer';
|
|
||||||
import AnimatedCounter from './animated-counter';
|
import AnimatedCounter from './animated-counter';
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
value: 'about.stats.items.2.value',
|
|
||||||
suffix: 'about.stats.items.2.suffix',
|
|
||||||
label: 'about.stats.items.2.label',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AboutCounter() {
|
export default function AboutCounter() {
|
||||||
const { t } = useTextController();
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const { sectionRef, isVisible } = useIntersectionObserver<HTMLDivElement>();
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const toNumber = (value: string) => {
|
const { t } = useTextController();
|
||||||
return Number(t(value));
|
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={sectionRef}
|
ref={sectionRef}
|
||||||
className='my-4 grid grid-cols-1 gap-6 text-center md:my-8'
|
className='my-4 grid grid-cols-1 gap-6 text-center md:my-8'
|
||||||
>
|
>
|
||||||
{stats.map((stat, index) => (
|
<div className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'>
|
||||||
<div
|
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
||||||
key={index}
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
className='transform rounded-lg bg-white p-3 shadow-md transition-transform hover:scale-105 sm:p-6'
|
</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'>
|
||||||
<div className='mb-4 inline-flex items-center justify-center rounded-full bg-red-100 p-2'>
|
<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' />
|
<Users className='h-6 w-6 text-red-600' />
|
||||||
</div>
|
</div>
|
||||||
<h3 className='text-2xl font-bold text-gray-900'>
|
<h3 className='text-2xl font-bold text-gray-900'>
|
||||||
{isVisible ? (
|
{isVisible ? (
|
||||||
<AnimatedCounter
|
<AnimatedCounter end={98} suffix='%' decimals={1} />
|
||||||
end={toNumber(stat.value)}
|
|
||||||
suffix={t(stat.suffix)}
|
|
||||||
decimals={stat.decimals || 0}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
`0${t(stat.suffix)}`
|
'0%'
|
||||||
)}
|
)}
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-gray-600'>{t(stat.label)}</p>
|
<p className='text-gray-600'>{t('about.stats.items.5.label')}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +0,0 @@
|
|||||||
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 };
|
|
||||||
};
|
|
||||||
@ -1,63 +1,69 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { Container } from '@/shared/components/container';
|
import { Container } from '@/shared/components/container';
|
||||||
import { useIntersectionObserver } from '@/shared/hooks/use-intersection-observer';
|
|
||||||
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
import { useTextController } from '@/shared/language/hooks/use-text-controller';
|
||||||
|
|
||||||
import AnimatedCounter from '../shared/components/animated-counter';
|
import AnimatedCounter from '../shared/components/animated-counter';
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
key: 'stations',
|
|
||||||
value: 'home.stats.stations.value',
|
|
||||||
suffix: 'home.stats.stations.suffix',
|
|
||||||
label: 'home.stats.stations',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export function StatsSection() {
|
export function StatsSection() {
|
||||||
const { t } = useTextController();
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const { sectionRef, isVisible } = useIntersectionObserver<HTMLDivElement>();
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const toNumber = (value: string) => Number(t(value));
|
const { t } = useTextController();
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<section ref={sectionRef} className='bg-red-600 text-white'>
|
<section ref={sectionRef} className='bg-red-600 text-white'>
|
||||||
<Container>
|
<Container>
|
||||||
<div className='grid grid-cols-2 gap-4 text-center sm:gap-8 md:grid-cols-4'>
|
<div className='grid grid-cols-2 gap-4 text-center sm:gap-8 md:grid-cols-4'>
|
||||||
{stats.map(({ key, value, suffix, label }) => (
|
<div className='space-y-2'>
|
||||||
<div key={key} className='space-y-2'>
|
|
||||||
<h3 className='text-3xl font-bold'>
|
<h3 className='text-3xl font-bold'>
|
||||||
{isVisible ? (
|
{isVisible ? <AnimatedCounter end={25} suffix='+' /> : '0+'}
|
||||||
<AnimatedCounter
|
|
||||||
end={toNumber(value)}
|
|
||||||
suffix={t(suffix) || undefined}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
`0${t(suffix) || ''}`
|
|
||||||
)}
|
|
||||||
</h3>
|
</h3>
|
||||||
<p className='text-sm text-white/80'>{t(label)}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user