feat(ui): update UI v1
This commit is contained in:
@@ -1,31 +1,68 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useInView } from 'framer-motion';
|
||||
import { useSite } from '../../api';
|
||||
import { useLang, formatNumber } from '../../i18n/lang';
|
||||
import { useFieldSaver } from '../../admin/AdminContext';
|
||||
import { AddItemButton, EditableItem } from '../../admin/Editable';
|
||||
import { InlineEdit } from '../admin/InlineEdit';
|
||||
|
||||
function Counter({ value }: {value: number;}) {
|
||||
const { lang } = useLang();
|
||||
function useCounter(targetNumber: number, duration: number = 2000) {
|
||||
const [count, setCount] = useState(0);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
const inView = useInView(ref, { once: true, margin: '-60px' });
|
||||
const [display, setDisplay] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inView) return;
|
||||
let frame = 0;
|
||||
const total = 60;
|
||||
const id = window.setInterval(() => {
|
||||
frame += 1;
|
||||
const progress = 1 - Math.pow(1 - frame / total, 3);
|
||||
setDisplay(Math.round(value * progress));
|
||||
if (frame >= total) window.clearInterval(id);
|
||||
}, 20);
|
||||
return () => window.clearInterval(id);
|
||||
}, [inView, value]);
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
|
||||
return <span ref={ref}>{formatNumber(display, lang)}</span>;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
observer.disconnect();
|
||||
|
||||
let startTime: number | null = null;
|
||||
const step = (timestamp: number) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const progress = timestamp - startTime;
|
||||
|
||||
// ease-out cubic
|
||||
const easeOut = progress >= duration ? 1 : 1 - Math.pow(1 - progress / duration, 3);
|
||||
|
||||
setCount(Math.floor(easeOut * targetNumber));
|
||||
|
||||
if (progress < duration) {
|
||||
window.requestAnimationFrame(step);
|
||||
} else {
|
||||
setCount(targetNumber);
|
||||
}
|
||||
};
|
||||
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, [targetNumber, duration]);
|
||||
|
||||
return { count, ref };
|
||||
}
|
||||
|
||||
function Counter({ value }: { value: string | number }) {
|
||||
const { lang } = useLang();
|
||||
|
||||
const strValue = String(value);
|
||||
// Match the first sequence of digits
|
||||
const numMatch = strValue.match(/(\d+)/);
|
||||
const targetNumber = numMatch ? parseInt(numMatch[0], 10) : 0;
|
||||
|
||||
const { count, ref } = useCounter(targetNumber, 1500);
|
||||
|
||||
// If there's a number, replace it with the animated (and localized) count, otherwise render string as is.
|
||||
const formattedDisplay = formatNumber(count, lang);
|
||||
const renderValue = numMatch ? strValue.replace(numMatch[0], formattedDisplay) : strValue;
|
||||
|
||||
return <span ref={ref}>{renderValue}</span>;
|
||||
}
|
||||
|
||||
export function Stats() {
|
||||
|
||||
Reference in New Issue
Block a user