rolling counter added
This commit is contained in:
parent
77f4ee0c48
commit
9d4178f1de
|
|
@ -1,67 +1,186 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useEffect, useState } from 'react';
|
import { useCallback, useMemo, useState, useEffect } from 'react';
|
||||||
|
|
||||||
interface RollingCounterProps {
|
interface RollingCounterProps {
|
||||||
numberString: string; // A 10-character string, e.g., "05-12-34-56-78"
|
numberString: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROLLS = 2;
|
||||||
|
const DIGIT_HEIGHT = 104;
|
||||||
|
const INITIAL_OFFSET = 38;
|
||||||
|
const EXTRA_NUMBERS_AFTER = 5;
|
||||||
|
const EXTRA_NUMBERS_BEFORE = 2;
|
||||||
|
|
||||||
|
const getNumbers = (targetValue: number, previousValue?: number) => {
|
||||||
|
const numbers = [];
|
||||||
|
|
||||||
|
if (previousValue === undefined) {
|
||||||
|
// Initial load
|
||||||
|
for (let i = 0; i < ROLLS; i++) {
|
||||||
|
for (let n = 0; n < 10; n++) {
|
||||||
|
numbers.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sequence before target
|
||||||
|
for (let n = EXTRA_NUMBERS_BEFORE; n > 0; n--) {
|
||||||
|
numbers.push((targetValue - n + 10) % 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
numbers.push(targetValue);
|
||||||
|
|
||||||
|
// Add extra numbers after target
|
||||||
|
for (let n = 1; n <= EXTRA_NUMBERS_AFTER; n++) {
|
||||||
|
numbers.push((targetValue + n) % 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep the previous sequence
|
||||||
|
for (let i = 0; i < ROLLS; i++) {
|
||||||
|
for (let n = 0; n < 10; n++) {
|
||||||
|
numbers.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sequence before previous value
|
||||||
|
for (let n = EXTRA_NUMBERS_BEFORE; n > 0; n--) {
|
||||||
|
numbers.push((previousValue - n + 10) % 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
numbers.push(previousValue);
|
||||||
|
|
||||||
|
// Add complete rolls between previous and target
|
||||||
|
for (let i = 0; i < ROLLS; i++) {
|
||||||
|
for (let n = 0; n < 10; n++) {
|
||||||
|
numbers.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sequence before target
|
||||||
|
for (let n = EXTRA_NUMBERS_BEFORE; n > 0; n--) {
|
||||||
|
numbers.push((targetValue - n + 10) % 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
numbers.push(targetValue);
|
||||||
|
|
||||||
|
// Add extra numbers after target
|
||||||
|
for (let n = 1; n <= EXTRA_NUMBERS_AFTER; n++) {
|
||||||
|
numbers.push((targetValue + n) % 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numbers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RollingDigit = ({
|
||||||
|
targetValue,
|
||||||
|
index,
|
||||||
|
onAnimationComplete,
|
||||||
|
isStopped,
|
||||||
|
showHyphen,
|
||||||
|
previousValue,
|
||||||
|
}: {
|
||||||
|
targetValue: number;
|
||||||
|
index: number;
|
||||||
|
onAnimationComplete: () => void;
|
||||||
|
isStopped: boolean;
|
||||||
|
showHyphen: boolean;
|
||||||
|
previousValue?: number;
|
||||||
|
}) => {
|
||||||
|
const numbers = useMemo(
|
||||||
|
() => getNumbers(targetValue, previousValue),
|
||||||
|
[targetValue, previousValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="overflow-hidden h-[180px] w-[77px] relative">
|
||||||
|
<motion.div
|
||||||
|
initial={false} // Don't reset to initial state
|
||||||
|
animate={{
|
||||||
|
y: -(numbers.length - EXTRA_NUMBERS_AFTER - 1) * DIGIT_HEIGHT + INITIAL_OFFSET,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
delay: index * 0.2,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}}
|
||||||
|
onAnimationComplete={onAnimationComplete}
|
||||||
|
className="absolute flex flex-col">
|
||||||
|
{numbers.map((num, i) => (
|
||||||
|
<div
|
||||||
|
key={`${index}-${i}`}
|
||||||
|
className={`h-[${DIGIT_HEIGHT}px] w-[77px] flex items-center justify-center numeric-display-1 transition-colors duration-500 ${
|
||||||
|
isStopped && num === targetValue ? 'text-white' : 'text-[#B0B1CD]'
|
||||||
|
}`}>
|
||||||
|
{num}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
{showHyphen && <div className="w-[32px] h-[4px] bg-lightOnPrimary mx-5 self-center" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const RollingCounter: React.FC<RollingCounterProps> = ({ numberString }) => {
|
const RollingCounter: React.FC<RollingCounterProps> = ({ numberString }) => {
|
||||||
const [rollingValues, setRollingValues] = useState<number[]>([]);
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
const [isStopped, setIsStopped] = useState<boolean[]>([]); // Track stopped numbers
|
const [isStopped, setIsStopped] = useState<boolean[]>([]);
|
||||||
|
const [previousNumbers, setPreviousNumbers] = useState<number[]>([]);
|
||||||
|
const [isNewNumber, setIsNewNumber] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Parse input string into an array of numbers, removing hyphens
|
if (!isInitialLoading) {
|
||||||
const targetNumbers = numberString
|
setPreviousNumbers(numbers);
|
||||||
|
setIsStopped(new Array(numberString.replace(/-/g, '').length).fill(false));
|
||||||
|
setIsNewNumber(true);
|
||||||
|
}
|
||||||
|
}, [numberString, isInitialLoading]);
|
||||||
|
|
||||||
|
const numbers = useMemo(() => {
|
||||||
|
if (!numberString) return [];
|
||||||
|
|
||||||
|
const parsed = numberString
|
||||||
.replace(/-/g, '')
|
.replace(/-/g, '')
|
||||||
.split('')
|
.split('')
|
||||||
.map((char) => parseInt(char, 10));
|
.map((char) => parseInt(char, 10));
|
||||||
setRollingValues(targetNumbers);
|
|
||||||
setIsStopped(new Array(targetNumbers.length).fill(false));
|
|
||||||
}, [numberString]);
|
|
||||||
|
|
||||||
const getNumbers = () => Array.from({ length: 10 }, (_, i) => i);
|
if (isInitialLoading) {
|
||||||
|
setIsStopped(new Array(parsed.length).fill(false));
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}, [numberString, isInitialLoading]);
|
||||||
|
|
||||||
|
const handleAnimationComplete = useCallback((index: number) => {
|
||||||
|
setIsStopped((prev) => {
|
||||||
|
const newState = [...prev];
|
||||||
|
newState[index] = true;
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isInitialLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center bg-lightPrimary text-white py-4 px-6 rounded-full">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center bg-lightPrimary text-white py-4 px-6 rounded-full">
|
<div className="flex items-center justify-center bg-lightPrimary text-white py-4 px-6 rounded-full">
|
||||||
{rollingValues.map((targetValue, index) => (
|
{numbers.map((num, index) => (
|
||||||
<div key={index} className="flex items-center justify-center">
|
<RollingDigit
|
||||||
{/* Container to display numbers */}
|
key={`${index}-${num}`}
|
||||||
<div className="overflow-hidden h-[180px] w-[77px] relative">
|
targetValue={num}
|
||||||
<motion.div
|
previousValue={previousNumbers[index]}
|
||||||
initial={{ y: '0%' }}
|
index={index}
|
||||||
animate={{ y: `-${targetValue * 10}%` }}
|
onAnimationComplete={() => handleAnimationComplete(index)}
|
||||||
transition={{
|
isStopped={isStopped[index]}
|
||||||
duration: 0.5 + index * 0.2,
|
showHyphen={(index + 1) % 2 === 0 && index !== numbers.length - 1}
|
||||||
ease: 'easeInOut',
|
/>
|
||||||
}}
|
|
||||||
onAnimationComplete={() => {
|
|
||||||
// Mark this number as stopped
|
|
||||||
setIsStopped((prev) => {
|
|
||||||
const newStatus = [...prev];
|
|
||||||
newStatus[index] = true;
|
|
||||||
return newStatus;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="absolute top-1/2 -translate-y-1/2 flex flex-col -mt-[52px]">
|
|
||||||
{getNumbers().map((num) => (
|
|
||||||
<div
|
|
||||||
key={num}
|
|
||||||
className={`px-4 py-2 numeric-display-1 transition-colors duration-500 ${
|
|
||||||
isStopped[index] && num === targetValue
|
|
||||||
? 'text-white' // Stopped number
|
|
||||||
: 'text-[#B0B1CD]' // Rolling numbers
|
|
||||||
}`}>
|
|
||||||
{num}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
{/* Add a hyphen every two digits */}
|
|
||||||
{(index + 1) % 2 === 0 && index !== rollingValues.length - 1 && (
|
|
||||||
<div className="w-[32px] h-[4px] bg-lightOnPrimary mx-5"></div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
'use client';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
interface RollingCounterWorkingProps {
|
||||||
|
numberString: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move constants outside component
|
||||||
|
const ROLLS = 5;
|
||||||
|
const DIGIT_HEIGHT = 104;
|
||||||
|
const INITIAL_OFFSET = 38;
|
||||||
|
const EXTRA_NUMBERS_AFTER = 5;
|
||||||
|
const EXTRA_NUMBERS_BEFORE = 2;
|
||||||
|
|
||||||
|
// Memoize number generation function
|
||||||
|
const getNumbers = (targetValue: number) => {
|
||||||
|
const numbers = [];
|
||||||
|
|
||||||
|
// Add complete rolls
|
||||||
|
for (let i = 0; i < ROLLS; i++) {
|
||||||
|
for (let n = 0; n < 10; n++) {
|
||||||
|
numbers.push(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sequence before target
|
||||||
|
for (let n = EXTRA_NUMBERS_BEFORE; n > 0; n--) {
|
||||||
|
numbers.push((targetValue - n + 10) % 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add target
|
||||||
|
numbers.push(targetValue);
|
||||||
|
|
||||||
|
// Add extra numbers after target
|
||||||
|
for (let n = 1; n <= EXTRA_NUMBERS_AFTER; n++) {
|
||||||
|
numbers.push((targetValue + n) % 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return numbers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RollingDigit = ({
|
||||||
|
targetValue,
|
||||||
|
index,
|
||||||
|
onAnimationComplete,
|
||||||
|
isStopped,
|
||||||
|
showHyphen,
|
||||||
|
}: {
|
||||||
|
targetValue: number;
|
||||||
|
index: number;
|
||||||
|
onAnimationComplete: () => void;
|
||||||
|
isStopped: boolean;
|
||||||
|
showHyphen: boolean;
|
||||||
|
}) => {
|
||||||
|
const numbers = useMemo(() => getNumbers(targetValue), [targetValue]);
|
||||||
|
|
||||||
|
const targetIndex = ROLLS * 10 + EXTRA_NUMBERS_BEFORE;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="overflow-hidden h-[180px] w-[77px] relative">
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: INITIAL_OFFSET }}
|
||||||
|
animate={{
|
||||||
|
y: -(targetIndex * DIGIT_HEIGHT) + INITIAL_OFFSET,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 5,
|
||||||
|
delay: index * 4,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
}}
|
||||||
|
onAnimationComplete={onAnimationComplete}
|
||||||
|
className="absolute flex flex-col">
|
||||||
|
{numbers.map((num, i) => (
|
||||||
|
<div
|
||||||
|
key={`${index}-${i}`}
|
||||||
|
className={`h-[${DIGIT_HEIGHT}px] w-[77px] flex items-center justify-center numeric-display-1 transition-colors duration-500 ${
|
||||||
|
isStopped && num === targetValue ? 'text-white' : 'text-[#B0B1CD]'
|
||||||
|
}`}>
|
||||||
|
{num}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
{showHyphen && <div className="w-[32px] h-[4px] bg-lightOnPrimary mx-5 self-center" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RollingCounterWorking: React.FC<RollingCounterWorkingProps> = ({ numberString }) => {
|
||||||
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
|
const [isStopped, setIsStopped] = useState<boolean[]>([]);
|
||||||
|
|
||||||
|
const numbers = useMemo(() => {
|
||||||
|
if (!numberString) return [];
|
||||||
|
|
||||||
|
const parsed = numberString
|
||||||
|
.replace(/-/g, '')
|
||||||
|
.split('')
|
||||||
|
.map((char) => parseInt(char, 10));
|
||||||
|
|
||||||
|
if (isInitialLoading) {
|
||||||
|
setIsStopped(new Array(parsed.length).fill(false));
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}, [numberString, isInitialLoading]);
|
||||||
|
|
||||||
|
const handleAnimationComplete = useCallback((index: number) => {
|
||||||
|
setIsStopped((prev) => {
|
||||||
|
const newState = [...prev];
|
||||||
|
newState[index] = true;
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isInitialLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center bg-lightPrimary text-white py-4 px-6 rounded-full">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center bg-lightPrimary text-white py-4 px-6 rounded-full">
|
||||||
|
{numbers.map((num, index) => (
|
||||||
|
<RollingDigit
|
||||||
|
key={`${index}-${num}`}
|
||||||
|
targetValue={num}
|
||||||
|
index={index}
|
||||||
|
onAnimationComplete={() => handleAnimationComplete(index)}
|
||||||
|
isStopped={isStopped[index]}
|
||||||
|
showHyphen={(index + 1) % 2 === 0 && index !== numbers.length - 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RollingCounterWorking;
|
||||||
Loading…
Reference in New Issue